diff --git a/DEPS b/DEPS
index e498d6f2..f29bf031 100644
--- a/DEPS
+++ b/DEPS
@@ -303,7 +303,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Skia
   # and whatever else without interference from each other.
-  'skia_revision': '063e339bedfcfb9d7c921cd92d9a902ef008ce76',
+  'skia_revision': 'dd4077962cd5456478501f733af12b5dfb17b3ea',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
@@ -311,7 +311,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ANGLE
   # and whatever else without interference from each other.
-  'angle_revision': '82c0ba9350b582eeff34c8c17886ac6b638f281b',
+  'angle_revision': '295eece61cce0f7721427d4a42e0fe1821c6d39f',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
@@ -818,7 +818,7 @@
 
   'src/clank': {
     'url': Var('chrome_git') + '/clank/internal/apps.git' + '@' +
-    '73c554959a79725f3f2e1a6f9a902bd4fc2febc3',
+    '20d5657767c7a726b9144002e4021d087725a89e',
     'condition': 'checkout_android and checkout_src_internal',
   },
 
@@ -980,7 +980,7 @@
     'packages': [
       {
           'package': 'chromium/third_party/androidx',
-          'version': 'Aym7zNxeVp_a5OUFZfShkxubeUpGPsS42gKXdKNJ7b4C',
+          'version': 'fxep2qUxHMuSadHbR8ufKuYVmB9SKknNkkBDLneqqhwC',
       },
     ],
     'condition': 'checkout_android',
@@ -1190,7 +1190,7 @@
   },
 
   'src/third_party/depot_tools':
-    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '86752e9a55281200715749d75a88cf57bf2e7b01',
+    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '259976c7483c321c122169d193e150310f4ea609',
 
   'src/third_party/devtools-frontend/src':
     Var('chromium_git') + '/devtools/devtools-frontend' + '@' + Var('devtools_frontend_revision'),
@@ -1838,7 +1838,7 @@
     Var('chromium_git') + '/external/khronosgroup/webgl.git' + '@' + 'f4bf599a8b575df685c31d9c4729a70a04e377ed',
 
   'src/third_party/webgpu-cts/src':
-    Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + 'f20c5f7b8f53904edaa98651d764e1b8305d7c14',
+    Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + 'ae15a59832989c22982acaeaccdf5d379afced62',
 
   'src/third_party/webrtc':
     Var('webrtc_git') + '/src.git' + '@' + '8e2ab67045941387e1b25d05f8fb219f524a80a8',
@@ -3944,7 +3944,7 @@
 
   'src/components/optimization_guide/internal': {
       'url': Var('chrome_git') + '/chrome/components/optimization_guide.git' + '@' +
-        'f35015d6f691b1aadc035cd1644aa3bc19a1a92a',
+        '22d6e7c48e3a433786898acedc2730a9b1c93e6c',
       'condition': 'checkout_src_internal',
   },
 
@@ -4004,7 +4004,7 @@
 
   'src/ios_internal':  {
       'url': Var('chrome_git') + '/chrome/ios_internal.git' + '@' +
-        'd8d82ca955498f2a3c907380b829aa6270a64b82',
+        '27d0f4f79518dfe9f8e15cf52f1130ba3a6a5958',
       'condition': 'checkout_ios and checkout_src_internal',
   },
 
diff --git a/android_webview/browser/gfx/aw_vulkan_context_provider.h b/android_webview/browser/gfx/aw_vulkan_context_provider.h
index 14c9db5..683b09a 100644
--- a/android_webview/browser/gfx/aw_vulkan_context_provider.h
+++ b/android_webview/browser/gfx/aw_vulkan_context_provider.h
@@ -8,7 +8,7 @@
 #include <memory>
 
 #include <optional>
-#include "base/memory/raw_ptr_exclusion.h"
+#include "base/memory/raw_ptr.h"
 #include "components/viz/common/gpu/vulkan_context_provider.h"
 #include "gpu/vulkan/vulkan_device_queue.h"
 #include "third_party/skia/include/core/SkRefCnt.h"
@@ -43,9 +43,7 @@
     ~ScopedSecondaryCBDraw() { provider_->SecondaryCMBDrawSubmitted(); }
 
    private:
-    // This field is not a raw_ptr<> because it was filtered by the rewriter
-    // for: #union
-    RAW_PTR_EXCLUSION AwVulkanContextProvider* const provider_;
+    raw_ptr<AwVulkanContextProvider> const provider_;
   };
 
   AwVulkanContextProvider(const AwVulkanContextProvider&) = delete;
diff --git a/android_webview/browser/gfx/overlay_processor_webview.h b/android_webview/browser/gfx/overlay_processor_webview.h
index e27ec41..a8be19272 100644
--- a/android_webview/browser/gfx/overlay_processor_webview.h
+++ b/android_webview/browser/gfx/overlay_processor_webview.h
@@ -7,7 +7,6 @@
 
 #include "android_webview/browser/gfx/display_scheduler_webview.h"
 #include "base/memory/raw_ptr.h"
-#include "base/memory/raw_ptr_exclusion.h"
 #include "base/memory/weak_ptr.h"
 #include "base/synchronization/waitable_event.h"
 #include "base/threading/thread_checker.h"
@@ -39,9 +38,7 @@
     ~ScopedSurfaceControlAvailable();
 
    private:
-    // This field is not a raw_ptr<> because it was filtered by the rewriter
-    // for: #union
-    RAW_PTR_EXCLUSION OverlayProcessorWebView* processor_;
+    raw_ptr<OverlayProcessorWebView> processor_;
   };
 
   OverlayProcessorWebView(
diff --git a/android_webview/browser/gfx/scoped_app_gl_state_restore_impl.cc b/android_webview/browser/gfx/scoped_app_gl_state_restore_impl.cc
index 1238e1a..211ebe3 100644
--- a/android_webview/browser/gfx/scoped_app_gl_state_restore_impl.cc
+++ b/android_webview/browser/gfx/scoped_app_gl_state_restore_impl.cc
@@ -284,7 +284,7 @@
     glGetVertexAttribiv(i, GL_VERTEX_ATTRIB_ARRAY_STRIDE,
                         &vertex_attrib_[i].stride);
     glGetVertexAttribPointerv(i, GL_VERTEX_ATTRIB_ARRAY_POINTER,
-                              &vertex_attrib_[i].pointer);
+                              &vertex_attrib_[i].pointer.AsEphemeralRawAddr());
     glGetVertexAttribfv(i, GL_CURRENT_VERTEX_ATTRIB,
                         vertex_attrib_[i].current_vertex_attrib);
   }
diff --git a/android_webview/browser/gfx/scoped_app_gl_state_restore_impl.h b/android_webview/browser/gfx/scoped_app_gl_state_restore_impl.h
index aa3d24f4..0a34e9e 100644
--- a/android_webview/browser/gfx/scoped_app_gl_state_restore_impl.h
+++ b/android_webview/browser/gfx/scoped_app_gl_state_restore_impl.h
@@ -9,7 +9,7 @@
 #include <vector>
 
 #include "android_webview/browser/gfx/scoped_app_gl_state_restore.h"
-#include "base/memory/raw_ptr_exclusion.h"
+#include "base/memory/raw_ptr.h"
 #include "ui/gl/gl_bindings.h"
 
 namespace android_webview {
@@ -43,9 +43,7 @@
     GLint type;
     GLint normalized;
     GLint stride;
-    // This field is not a raw_ptr<> because it was filtered by the rewriter
-    // for: #addr-of
-    RAW_PTR_EXCLUSION GLvoid* pointer;
+    raw_ptr<GLvoid> pointer;
     GLint vertex_attrib_array_buffer_binding;
     GLfloat current_vertex_attrib[4];
   };
diff --git a/android_webview/browser/tracing/aw_trace_event_args_allowlist.cc b/android_webview/browser/tracing/aw_trace_event_args_allowlist.cc
index df51950..de8a545 100644
--- a/android_webview/browser/tracing/aw_trace_event_args_allowlist.cc
+++ b/android_webview/browser/tracing/aw_trace_event_args_allowlist.cc
@@ -5,7 +5,7 @@
 #include "android_webview/browser/tracing/aw_trace_event_args_allowlist.h"
 
 #include "base/functional/bind.h"
-#include "base/memory/raw_ptr_exclusion.h"
+#include "base/memory/raw_ptr.h"
 #include "base/strings/pattern.h"
 #include "base/strings/string_tokenizer.h"
 #include "base/strings/string_util.h"
@@ -16,9 +16,7 @@
 struct AllowlistEntry {
   const char* category_name;
   const char* event_name;
-  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
-  // #global-scope
-  RAW_PTR_EXCLUSION const char* const* arg_name_filter;
+  raw_ptr<const char* const> arg_name_filter;
 };
 
 const char* const kMemoryDumpAllowedArgs[] = {"dumps", nullptr};
diff --git a/android_webview/js_sandbox/service/js_sandbox_isolate.h b/android_webview/js_sandbox/service/js_sandbox_isolate.h
index 426f398..f381e45 100644
--- a/android_webview/js_sandbox/service/js_sandbox_isolate.h
+++ b/android_webview/js_sandbox/service/js_sandbox_isolate.h
@@ -14,7 +14,6 @@
 #include "base/compiler_specific.h"
 #include "base/files/scoped_file.h"
 #include "base/functional/callback_forward.h"
-#include "base/memory/raw_ptr_exclusion.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/synchronization/lock.h"
 #include "base/thread_annotations.h"
diff --git a/android_webview/nonembedded/component_updater/aw_component_updater_configurator.cc b/android_webview/nonembedded/component_updater/aw_component_updater_configurator.cc
index 2fa0fee..82a5f9e 100644
--- a/android_webview/nonembedded/component_updater/aw_component_updater_configurator.cc
+++ b/android_webview/nonembedded/component_updater/aw_component_updater_configurator.cc
@@ -5,10 +5,10 @@
 #include "android_webview/nonembedded/component_updater/aw_component_updater_configurator.h"
 
 #include <memory>
+#include <optional>
 #include <string>
 #include <vector>
 
-#include <optional>
 #include "android_webview/nonembedded/net/network_impl.h"
 #include "base/android/path_utils.h"
 #include "base/files/file_path.h"
@@ -210,4 +210,8 @@
              : std::nullopt;
 }
 
+bool AwComponentUpdaterConfigurator::IsConnectionMetered() const {
+  return configurator_impl_.IsConnectionMetered();
+}
+
 }  // namespace android_webview
diff --git a/android_webview/nonembedded/component_updater/aw_component_updater_configurator.h b/android_webview/nonembedded/component_updater/aw_component_updater_configurator.h
index 5ca90de..192a021 100644
--- a/android_webview/nonembedded/component_updater/aw_component_updater_configurator.h
+++ b/android_webview/nonembedded/component_updater/aw_component_updater_configurator.h
@@ -67,6 +67,7 @@
   std::optional<bool> IsMachineExternallyManaged() const override;
   update_client::UpdaterStateProvider GetUpdaterStateProvider() const override;
   std::optional<base::FilePath> GetCrxCachePath() const override;
+  bool IsConnectionMetered() const override;
 
  protected:
   friend class base::RefCountedThreadSafe<AwComponentUpdaterConfigurator>;
diff --git a/ash/BUILD.gn b/ash/BUILD.gn
index b4521e1..ca2a2f7 100644
--- a/ash/BUILD.gn
+++ b/ash/BUILD.gn
@@ -2568,16 +2568,16 @@
     "wm/native_cursor_manager_ash.h",
     "wm/overlay_layout_manager.cc",
     "wm/overlay_layout_manager.h",
+    "wm/overview/birch/birch_bar_view.cc",
+    "wm/overview/birch/birch_bar_view.h",
+    "wm/overview/birch/birch_chip_button.cc",
+    "wm/overview/birch/birch_chip_button.h",
     "wm/overview/cleanup_animation_observer.cc",
     "wm/overview/cleanup_animation_observer.h",
     "wm/overview/delayed_animation_observer.h",
     "wm/overview/delayed_animation_observer_impl.cc",
     "wm/overview/delayed_animation_observer_impl.h",
     "wm/overview/event_handler_delegate.h",
-    "wm/overview/glanceables/glanceables_bar_view.cc",
-    "wm/overview/glanceables/glanceables_bar_view.h",
-    "wm/overview/glanceables/glanceables_chip_button.cc",
-    "wm/overview/glanceables/glanceables_chip_button.h",
     "wm/overview/overview_animation_state_waiter.cc",
     "wm/overview/overview_animation_state_waiter.h",
     "wm/overview/overview_constants.h",
diff --git a/ash/accelerators/key_hold_detector.cc b/ash/accelerators/key_hold_detector.cc
index 3587f0b..a7d919e8e 100644
--- a/ash/accelerators/key_hold_detector.cc
+++ b/ash/accelerators/key_hold_detector.cc
@@ -57,7 +57,7 @@
       case INITIAL:
         // Pass through posted event.
         if (event->flags() & ui::EF_IS_SYNTHESIZED) {
-          event->set_flags(event->flags() & ~ui::EF_IS_SYNTHESIZED);
+          event->SetFlags(event->flags() & ~ui::EF_IS_SYNTHESIZED);
           return;
         }
         state_ = PRESSED;
diff --git a/ash/accessibility/chromevox/touch_exploration_controller.cc b/ash/accessibility/chromevox/touch_exploration_controller.cc
index 3f35041..296a157 100644
--- a/ash/accessibility/chromevox/touch_exploration_controller.cc
+++ b/ash/accessibility/chromevox/touch_exploration_controller.cc
@@ -51,7 +51,7 @@
 void SetTouchAccessibilityFlag(ui::Event* event) {
   // This flag is used to identify mouse move events that were generated from
   // touch exploration in Chrome code.
-  event->set_flags(event->flags() | ui::EF_TOUCH_ACCESSIBILITY);
+  event->SetFlags(event->flags() | ui::EF_TOUCH_ACCESSIBILITY);
 }
 
 std::unique_ptr<ui::GestureProviderAura> BuildGestureProviderAura(
@@ -190,7 +190,7 @@
                                  touch_event.pointer_details());
         new_event.set_location(location);
         new_event.set_root_location(root_location);
-        new_event.set_flags(touch_event.flags());
+        new_event.SetFlags(touch_event.flags());
         return SendEventFinally(continuation, &new_event);
       }
 
diff --git a/ash/accessibility/magnifier/fullscreen_magnifier_controller.cc b/ash/accessibility/magnifier/fullscreen_magnifier_controller.cc
index ebf607e..261abe8 100644
--- a/ash/accessibility/magnifier/fullscreen_magnifier_controller.cc
+++ b/ash/accessibility/magnifier/fullscreen_magnifier_controller.cc
@@ -438,7 +438,7 @@
                                         it.second->pointer_details());
       touch_cancel_event.set_location_f(it.second->location_f());
       touch_cancel_event.set_root_location_f(it.second->root_location_f());
-      touch_cancel_event.set_flags(it.second->flags());
+      touch_cancel_event.SetFlags(it.second->flags());
 
       // TouchExplorationController is watching event stream and managing its
       // internal state. If an event rewriter (FullscreenMagnifierController)
diff --git a/ash/accessibility/sticky_keys/sticky_keys_controller.cc b/ash/accessibility/sticky_keys/sticky_keys_controller.cc
index 9aef323..2cfb2c9 100644
--- a/ash/accessibility/sticky_keys/sticky_keys_controller.cc
+++ b/ash/accessibility/sticky_keys/sticky_keys_controller.cc
@@ -42,7 +42,7 @@
 
   *rewritten_event = event.Clone();
   if (mod_down_flags & ~event.flags()) {
-    (*rewritten_event)->set_flags(event.flags() | mod_down_flags);
+    (*rewritten_event)->SetFlags(event.flags() | mod_down_flags);
   }
 
   if (released)
diff --git a/ash/ambient/OWNERS b/ash/ambient/OWNERS
index 574ed11..591b2e9 100644
--- a/ash/ambient/OWNERS
+++ b/ash/ambient/OWNERS
@@ -1 +1,8 @@
-file://chromeos/ash/components/assistant/OWNERS
+# Please use this reviewer queue instead of specifying an individual reviewer
+# unless there is a specific reason for it.
+assistive-code-review@google.com
+
+cowmoo@google.com #{LAST_RESORT_SUGGESTION}
+esum@google.com #{LAST_RESORT_SUGGESTION}
+wutao@chromium.org #{LAST_RESORT_SUGGESTION}
+xiaohuic@chromium.org #{LAST_RESORT_SUGGESTION}
diff --git a/ash/constants/ash_features.cc b/ash/constants/ash_features.cc
index 8be7791..889b6ce3 100644
--- a/ash/constants/ash_features.cc
+++ b/ash/constants/ash_features.cc
@@ -1470,8 +1470,8 @@
 // Ignores the rate limiting of holding space wallpaper nudge so that it will
 // show every time a user drags a file over the wallpaper. Enabling this flag
 // does nothing unless `kHoldingSpaceWallpaperNudge` is also enabled.
-BASE_FEATURE(kHoldingSpaceWallpaperNudgeIgnoreRateLimiting,
-             "HoldingSpaceWallpaperNudgeIgnoreRateLimiting",
+BASE_FEATURE(kHoldingSpaceWallpaperNudgeForceEligibility,
+             "HoldingSpaceWallpaperNudgeForceEligibility",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
 BASE_FEATURE(kHomeButtonQuickAppAccess,
@@ -3563,10 +3563,10 @@
          kHoldingSpaceWallpaperNudgeEnabledCounterfactually.Get();
 }
 
-bool IsHoldingSpaceWallpaperNudgeRateLimitingEnabled() {
+bool IsHoldingSpaceWallpaperNudgeForceEligibilityEnabled() {
   return IsHoldingSpaceWallpaperNudgeEnabled() &&
-         !base::FeatureList::IsEnabled(
-             kHoldingSpaceWallpaperNudgeIgnoreRateLimiting);
+         base::FeatureList::IsEnabled(
+             kHoldingSpaceWallpaperNudgeForceEligibility);
 }
 
 bool IsHomeButtonQuickAppAccessEnabled() {
diff --git a/ash/constants/ash_features.h b/ash/constants/ash_features.h
index 2a91e39..1c12de36 100644
--- a/ash/constants/ash_features.h
+++ b/ash/constants/ash_features.h
@@ -474,7 +474,7 @@
 COMPONENT_EXPORT(ASH_CONSTANTS)
 BASE_DECLARE_FEATURE(kHoldingSpaceWallpaperNudge);
 COMPONENT_EXPORT(ASH_CONSTANTS)
-BASE_DECLARE_FEATURE(kHoldingSpaceWallpaperNudgeIgnoreRateLimiting);
+BASE_DECLARE_FEATURE(kHoldingSpaceWallpaperNudgeForceEligibility);
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kHomeButtonQuickAppAccess);
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kHotspot);
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kVirtualKeyboardNewHeader);
@@ -1055,7 +1055,7 @@
 COMPONENT_EXPORT(ASH_CONSTANTS)
 bool IsHoldingSpaceWallpaperNudgeEnabledCounterfactually();
 COMPONENT_EXPORT(ASH_CONSTANTS)
-bool IsHoldingSpaceWallpaperNudgeRateLimitingEnabled();
+bool IsHoldingSpaceWallpaperNudgeForceEligibilityEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS)
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsHomeButtonQuickAppAccessEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsHomeButtonWithTextEnabled();
diff --git a/ash/drag_drop/drag_drop_controller.cc b/ash/drag_drop/drag_drop_controller.cc
index 2b019ac..8865ca4f 100644
--- a/ash/drag_drop/drag_drop_controller.cc
+++ b/ash/drag_drop/drag_drop_controller.cc
@@ -536,7 +536,7 @@
                                     const ui::LocatedEvent& event) {
   ui::DropTargetEvent e(*drag_data_.get(), event.location_f(),
                         event.root_location_f(), allowed_operations_);
-  e.set_flags(event.flags());
+  e.SetFlags(event.flags());
   ui::Event::DispatcherApi(&e).set_target(target);
 
   aura::client::DragUpdateInfo drag_info;
@@ -623,7 +623,7 @@
 
   ui::DropTargetEvent e(*drag_data_.get(), event.location_f(),
                         event.root_location_f(), allowed_operations_);
-  e.set_flags(event.flags());
+  e.SetFlags(event.flags());
   ui::Event::DispatcherApi(&e).set_target(target);
 
   for (aura::client::DragDropClientObserver& observer : observers_) {
diff --git a/ash/events/peripheral_customization_event_rewriter.cc b/ash/events/peripheral_customization_event_rewriter.cc
index b5197f5..ea2e927 100644
--- a/ash/events/peripheral_customization_event_rewriter.cc
+++ b/ash/events/peripheral_customization_event_rewriter.cc
@@ -721,7 +721,7 @@
     std::unique_ptr<ui::Event> rewritten_event = CloneEvent(mouse_event);
     const int remappable_flags =
         GetRemappableMouseEventFlags(*device_type_to_observe);
-    rewritten_event->set_flags(rewritten_event->flags() & ~remappable_flags);
+    rewritten_event->SetFlags(rewritten_event->flags() & ~remappable_flags);
     if (rewritten_event->IsMouseEvent()) {
       auto& rewritten_mouse_event = *rewritten_event->AsMouseEvent();
       rewritten_mouse_event.set_changed_button_flags(
@@ -818,7 +818,7 @@
   // remapped, this will behave incorrectly as it will remove "Ctrl". Instead,
   // this needs to track what keys are being pressed by the device that have
   // modifiers attached to them. For now, this is close enough to being correct.
-  event.set_flags(event.flags() & ~modifier_flags);
+  event.SetFlags(event.flags() & ~modifier_flags);
 }
 
 void PeripheralCustomizationEventRewriter::ApplyRemappedModifiers(
@@ -827,7 +827,7 @@
   for (const auto& [_, flag] : device_button_to_flags_) {
     flags |= flag;
   }
-  event.set_flags(event.flags() | flags);
+  event.SetFlags(event.flags() | flags);
 }
 
 std::unique_ptr<ui::Event> PeripheralCustomizationEventRewriter::CloneEvent(
diff --git a/ash/system/holding_space/holding_space_tray_unittest.cc b/ash/system/holding_space/holding_space_tray_unittest.cc
index 1ce903e..28ba8ab 100644
--- a/ash/system/holding_space/holding_space_tray_unittest.cc
+++ b/ash/system/holding_space/holding_space_tray_unittest.cc
@@ -612,6 +612,27 @@
 
 // Tests -----------------------------------------------------------------------
 
+// Holding Space used to own the constant which determines its bubble's width
+// but now shares a constant with the rest of the system UI bubbles. Holding
+// Space UI is not yet implemented to be fully reactive to variable bubble
+// widths, so this test adds a speed bump to (hopefully) prevent the shared
+// constant from being updated and inadvertently breaking Holding Space UI.
+TEST_F(HoldingSpaceTrayTest, BubbleHasExpectedWidth) {
+  // Start session and verify the holding space tray is showing in the shelf.
+  StartSession(/*pre_mark_time_of_first_add=*/true);
+  EXPECT_TRUE(test_api()->IsShowingInShelf());
+
+  // Show the holding space bubble.
+  test_api()->Show();
+  EXPECT_TRUE(test_api()->IsShowing());
+
+  // Verify holding space bubble width.
+  views::View* const bubble = test_api()->GetBubble();
+  ASSERT_TRUE(bubble);
+  ViewDrawnWaiter().Wait(bubble);
+  EXPECT_EQ(bubble->width(), 360);
+}
+
 TEST_F(HoldingSpaceTrayTest, ShowTrayButtonWhenForced) {
   // Case: Force show in shelf prior to session start.
   auto force_show_in_shelf =
diff --git a/ash/user_education/holding_space_wallpaper_nudge/holding_space_wallpaper_nudge_controller.cc b/ash/user_education/holding_space_wallpaper_nudge/holding_space_wallpaper_nudge_controller.cc
index afd80e6..39d72d9 100644
--- a/ash/user_education/holding_space_wallpaper_nudge/holding_space_wallpaper_nudge_controller.cc
+++ b/ash/user_education/holding_space_wallpaper_nudge/holding_space_wallpaper_nudge_controller.cc
@@ -137,7 +137,7 @@
 // before. It should be no more than once in a 24 hour period, no more than 3
 // times total, and never if the user has pinned a file before.
 bool NudgeShouldBeShown() {
-  if (!features::IsHoldingSpaceWallpaperNudgeRateLimitingEnabled()) {
+  if (features::IsHoldingSpaceWallpaperNudgeForceEligibilityEnabled()) {
     return true;
   }
 
diff --git a/ash/user_education/holding_space_wallpaper_nudge/holding_space_wallpaper_nudge_controller_unittest.cc b/ash/user_education/holding_space_wallpaper_nudge/holding_space_wallpaper_nudge_controller_unittest.cc
index b7615b923..6151adf 100644
--- a/ash/user_education/holding_space_wallpaper_nudge/holding_space_wallpaper_nudge_controller_unittest.cc
+++ b/ash/user_education/holding_space_wallpaper_nudge/holding_space_wallpaper_nudge_controller_unittest.cc
@@ -270,7 +270,7 @@
   HoldingSpaceWallpaperNudgeControllerTestBase(
       std::optional<bool> counterfactual_enabled,
       std::optional<bool> drop_to_pin_enabled,
-      bool rate_limiting_enabled,
+      bool force_eligibility_enabled,
       base::test::TaskEnvironment::TimeSource time_source)
       : UserEducationAshTestBase(time_source) {
     // NOTE: The `HoldingSpaceWallpaperNudgeController` exists only when the
@@ -292,13 +292,13 @@
 
     enabled.emplace_back(features::kHoldingSpaceWallpaperNudge, params);
 
-    if (rate_limiting_enabled) {
-      disabled.emplace_back(
-          features::kHoldingSpaceWallpaperNudgeIgnoreRateLimiting);
-    } else {
+    if (force_eligibility_enabled) {
       enabled.emplace_back(
-          features::kHoldingSpaceWallpaperNudgeIgnoreRateLimiting,
+          features::kHoldingSpaceWallpaperNudgeForceEligibility,
           base::FieldTrialParams());
+    } else {
+      disabled.emplace_back(
+          features::kHoldingSpaceWallpaperNudgeForceEligibility);
     }
 
     scoped_feature_list_.InitWithFeaturesAndParameters(enabled, disabled);
@@ -437,7 +437,7 @@
       : HoldingSpaceWallpaperNudgeControllerTestBase(
             /*counterfactual_enabled=*/false,
             /*drop_to_pin_enabled=*/false,
-            /*rate_limiting_enabled=*/true,
+            /*force_eligibility_enabled=*/false,
             base::test::TaskEnvironment::TimeSource::SYSTEM_TIME) {}
 };
 
@@ -551,7 +551,7 @@
       : HoldingSpaceWallpaperNudgeControllerTestBase(
             /*counterfactual_enabled=*/false,
             drop_to_pin_enabled(),
-            /*rate_limiting_enabled=*/false,
+            /*force_eligibility_enabled=*/true,
             base::test::TaskEnvironment::TimeSource::SYSTEM_TIME) {}
 
   // Whether the drop-to-pin feature param is enabled.
@@ -830,7 +830,7 @@
       : HoldingSpaceWallpaperNudgeControllerTestBase(
             /*counterfactual_enabled=*/false,
             drop_to_pin_enabled(),
-            /*rate_limiting_enabled=*/true,
+            /*force_eligibility_enabled=*/false,
             base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
 
   // Whether the drop-to-pin feature param is enabled.
@@ -1052,7 +1052,7 @@
       : HoldingSpaceWallpaperNudgeControllerTestBase(
             counterfactual_enabled(),
             drop_to_pin_enabled(),
-            /*rate_limiting_enabled=*/false,
+            /*force_eligibility_enabled=*/true,
             base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
 
   // Whether the is-counterfactual feature parameter is enabled.
diff --git a/ash/webui/common/resources/cellular_setup/activation_code_page.js b/ash/webui/common/resources/cellular_setup/activation_code_page.js
index aa085cb..8ff336286 100644
--- a/ash/webui/common/resources/cellular_setup/activation_code_page.js
+++ b/ash/webui/common/resources/cellular_setup/activation_code_page.js
@@ -14,10 +14,10 @@
 import './cellular_setup_icons.html.js';
 
 import {focusWithoutInk} from '//resources/ash/common/focus_without_ink_js.js';
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
 import {loadTimeData} from '//resources/ash/common/load_time_data.m.js';
 import {MojoInterfaceProviderImpl} from '//resources/ash/common/network/mojo_interface_provider.js';
-import {afterNextRender, Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {afterNextRender, mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {CrosNetworkConfig, CrosNetworkConfigInterface} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
 import {NetworkType} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
 
@@ -66,146 +66,196 @@
  */
 const ACTIVATION_CODE_PREFIX = 'LPA:1$';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'activation-code-page',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const ActivationCodePageElementBase =
+    mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
+/** @polymer */
+class ActivationCodePageElement extends ActivationCodePageElementBase {
+  static get is() {
+    return 'activation-code-page';
+  }
 
-  properties: {
-    activationCode: {
-      type: String,
-      notify: true,
-      observer: 'onActivationCodeChanged_',
-    },
+  static get template() {
+    return getTemplate();
+  }
 
-    showError: {
-      type: Boolean,
-      notify: true,
-      observer: 'onShowErrorChanged_',
-    },
+  static get properties() {
+    return {
+      activationCode: {
+        type: String,
+        notify: true,
+        observer: 'onActivationCodeChanged_',
+      },
+
+      showError: {
+        type: Boolean,
+        notify: true,
+        observer: 'onShowErrorChanged_',
+      },
+
+      /**
+       * Readonly property indicating whether the current |activationCode|
+       * was scanned from QR code.
+       */
+      isFromQrCode: {
+        type: Boolean,
+        notify: true,
+        value: false,
+      },
+
+      /**
+       * Indicates the UI is busy with an operation and cannot be interacted
+       * with.
+       */
+      showBusy: {
+        type: Boolean,
+        value: false,
+      },
+
+      /**
+       * Indicates no profiles were found while scanning.
+       */
+      showNoProfilesFound: {
+        type: Boolean,
+        notify: true,
+      },
+
+      /**
+       * Enum used as an ID for specific UI elements.
+       * A UiElement is passed between html and JS for
+       * certain UI elements to determine their state.
+       *
+       * @type {!UiElement}
+       */
+      UiElement: {
+        type: Object,
+        value: UiElement,
+      },
+
+      /**
+       * @type {!PageState}
+       * @private
+       */
+      state_: {
+        type: Object,
+        value: PageState,
+        observer: 'onStateChanged_',
+      },
+
+      /** @private */
+      cameraCount_: {
+        type: Number,
+        value: 0,
+        observer: 'onHasCameraCountChanged_',
+      },
+
+      /**
+       *  TODO(crbug.com/1093185): add type |BarcodeDetector| when externs
+       *  becomes available
+       *  @private {?Object}
+       */
+      qrCodeDetector_: {
+        type: Object,
+        value: null,
+      },
+
+      /**
+       * If true, video is expanded.
+       */
+      expanded_: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+
+      /**
+       * A11y string used to announce the current status of qr code camera
+       * detection. Used when device web cam is turned on and ready to scan,
+       * and also used after scan has been completed.
+       * @private
+       */
+      qrCodeCameraA11yString_: {
+        type: String,
+        value: '',
+      },
+
+      /**
+       * If true, device is locked to specific cellular operator.
+       */
+      isDeviceCarrierLocked_: {
+        type: Boolean,
+        value: false,
+      },
+
+      isCellularCarrierLockEnabled_: {
+        type: Boolean,
+        value() {
+          return loadTimeData.valueExists('isCellularCarrierLockEnabled') &&
+              loadTimeData.getBoolean('isCellularCarrierLockEnabled');
+        },
+      },
+
+      /**
+       * Indicates whether or not |activationCode| matches the correct
+       * activation code format. If there is a partial match (i.e. the code is
+       * incomplete but matches the format so far), this will be false.
+       * @private
+       */
+      isActivationCodeInvalidFormat_: {
+        type: Boolean,
+        value: false,
+      },
+    };
+  }
+
+  /** @override */
+  constructor() {
+    super();
 
     /**
-     * Readonly property indicating whether the current |activationCode|
-     * was scanned from QR code.
-     */
-    isFromQrCode: {
-      type: Boolean,
-      notify: true,
-      value: false,
-    },
-
-    /**
-     * Indicates the UI is busy with an operation and cannot be interacted with.
-     */
-    showBusy: {
-      type: Boolean,
-      value: false,
-    },
-
-    /**
-     * Indicates no profiles were found while scanning.
-     */
-    showNoProfilesFound: {
-      type: Boolean,
-      notify: true,
-    },
-
-    /**
-     * Enum used as an ID for specific UI elements.
-     * A UiElement is passed between html and JS for
-     * certain UI elements to determine their state.
-     *
-     * @type {!UiElement}
-     */
-    UiElement: {
-      type: Object,
-      value: UiElement,
-    },
-
-    /**
-     * @type {!PageState}
+     * @type {MediaDevices}
      * @private
      */
-    state_: {
-      type: Object,
-      value: PageState,
-      observer: 'onStateChanged_',
-    },
+    this.mediaDevices_ = null;
 
-    /** @private */
-    cameraCount_: {
-      type: Number,
-      value: 0,
-      observer: 'onHasCameraCountChanged_',
-    },
+    /**
+     * @type {?MediaStream}
+     * @private
+     */
+    this.stream_ = null;
+
+    /**
+     * @type {?number}
+     * @private
+     */
+    this.qrCodeDetectorTimer_ = null;
+
+    /**
+     * The function used to initiate a repeating timer. Can be overwritten in
+     * tests.
+     * @private {function(Function, number)}
+     */
+    this.setIntervalFunction_ = setInterval.bind(window);
 
     /**
      *  TODO(crbug.com/1093185): add type |BarcodeDetector| when externs
      *  becomes available
-     *  @private {?Object}
+     *  @suppress {undefinedVars|missingProperties}
+     *  @private
      */
-    qrCodeDetector_: {
-      type: Object,
-      value: null,
-    },
+    this.barcodeDetectorClass_ = BarcodeDetector;
 
-    /**
-     * If true, video is expanded.
-     */
-    expanded_: {
-      type: Boolean,
-      value: false,
-      reflectToAttribute: true,
-    },
-
-    /**
-     * A11y string used to announce the current status of qr code camera
-     * detection. Used when device web cam is turned on and ready to scan,
-     * and also used after scan has been completed.
-     * @private
-     */
-    qrCodeCameraA11yString_: {
-      type: String,
-      value: '',
-    },
-
-    /**
-     * If true, device is locked to specific cellular operator.
-     */
-    isDeviceCarrierLocked_: {
-      type: Boolean,
-      value: false,
-    },
-
-    isCellularCarrierLockEnabled_: {
-      type: Boolean,
-      value() {
-        return loadTimeData.valueExists('isCellularCarrierLockEnabled') &&
-            loadTimeData.getBoolean('isCellularCarrierLockEnabled');
-      },
-    },
-
-    /**
-     * Indicates whether or not |activationCode| matches the correct activation
-     * code format. If there is a partial match (i.e. the code is incomplete but
-     * matches the format so far), this will be false.
-     * @private
-     */
-    isActivationCodeInvalidFormat_: {
-      type: Boolean,
-      value: false,
-    },
-  },
-
-  /** @private {?CrosNetworkConfigInterface} */
-  networkConfig_: null,
-
-  /** @override */
-  created() {
+    /** @private {typeof ImageCapture} */
+    this.imageCaptureClass_ = ImageCapture;
     if (!this.isCellularCarrierLockEnabled_) {
       return;
     }
+
     this.networkConfig_ =
         MojoInterfaceProviderImpl.getInstance().getMojoServiceRemote();
     this.networkConfig_.getDeviceStateList().then(response => {
@@ -216,80 +266,51 @@
         this.isDeviceCarrierLocked_ = deviceState.isCarrierLocked;
       }
     });
-  },
-
-  /**
-   * @type {MediaDevices}
-   * @private
-   */
-  mediaDevices_: null,
-
-  /**
-   * @type {?MediaStream}
-   * @private
-   */
-  stream_: null,
-
-  /**
-   * @type {?number}
-   * @private
-   */
-  qrCodeDetectorTimer_: null,
-
-
-  /**
-   * The function used to initiate a repeating timer. Can be overwritten in
-   * tests.
-   * @private {function(Function, number)}
-   */
-  setIntervalFunction_: setInterval.bind(window),
-
-  /**
-   *  TODO(crbug.com/1093185): add type |BarcodeDetector| when externs
-   *  becomes available
-   *  @suppress {undefinedVars|missingProperties}
-   *  @private
-   */
-  barcodeDetectorClass_: BarcodeDetector,
-
-  /** @private {typeof ImageCapture} */
-  imageCaptureClass_: ImageCapture,
-
-  /**
-   * Function used to play the video. Can be overwritten by
-   * setFakesForTesting().
-   * @private {function()}
-   */
-  playVideo_: function() {
-    this.$$('#video').play();
-  },
-
-  /**
-   * Function used to stop a stream. Can be overwritten by setFakesForTesting().
-   * @private {function(MediaStream)}
-   */
-  stopStream_: function(stream) {
-    if (stream) {
-      stream.getTracks()[0].stop();
-    }
-  },
+  }
 
   /** @override */
   ready() {
+    super.ready();
+
     this.setMediaDevices(navigator.mediaDevices);
     this.initBarcodeDetector_();
     this.state_ = PageState.MANUAL_ENTRY;
-  },
+  }
 
   /** @override */
-  detached() {
+  disconnectedCallback() {
+    super.disconnectedCallback();
+
     this.stopStream_(this.stream_);
     if (this.qrCodeDetectorTimer_) {
       this.clearQrCodeDetectorTimer_();
     }
     this.mediaDevices_.removeEventListener(
         'devicechange', this.updateCameraCount_.bind(this));
-  },
+  }
+
+  /**
+   * Function used to play the video. Can be overwritten by
+   * setFakesForTesting().
+   * @private
+   */
+  playVideo_() {
+    const videoElement = this.shadowRoot.querySelector('#video');
+    if (videoElement) {
+      videoElement.play();
+    }
+  }
+
+  /**
+   * Function used to stop a stream. Can be overwritten by setFakesForTesting().
+   * @param {MediaStream} stream
+   * @private
+   */
+  stopStream_(stream) {
+    if (stream) {
+      stream.getTracks()[0].stop();
+    }
+  }
 
   /**
    * @return {boolean}
@@ -297,7 +318,7 @@
    */
   isScanningAvailable_() {
     return this.cameraCount_ > 0 && !!this.qrCodeDetector_;
-  },
+  }
 
   /**
    * @return {boolean}
@@ -305,7 +326,7 @@
    */
   shouldShowCarrierLockWarning_() {
     return this.isCellularCarrierLockEnabled_ && this.isDeviceCarrierLocked_;
-  },
+  }
 
   /**
    * TODO(crbug.com/1093185): Remove suppression when shape_detection extern
@@ -326,7 +347,7 @@
       this.qrCodeDetector_ =
           new this.barcodeDetectorClass_({formats: [QR_CODE_FORMAT]});
     }
-  },
+  }
 
   /**
    * @param {MediaDevices} mediaDevices
@@ -336,7 +357,7 @@
     this.updateCameraCount_();
     this.mediaDevices_.addEventListener(
         'devicechange', this.updateCameraCount_.bind(this));
-  },
+  }
 
   /**
    * TODO(crbug.com/1093185): Add barcodeDetectorClass type when BarcodeDetector
@@ -356,14 +377,14 @@
     this.setIntervalFunction_ = setIntervalFunction;
     this.playVideo_ = playVideoFunction;
     this.stopStream_ = stopStreamFunction;
-  },
+  }
 
   /**
    * @returns {?number}
    */
   getQrCodeDetectorTimerForTest() {
     return this.qrCodeDetectorTimer_;
-  },
+  }
 
   /**
    * @return {string}
@@ -371,7 +392,7 @@
    */
   computeActivationCodeClass_() {
     return this.isScanningAvailable_() ? 'relative' : 'center width-92';
-  },
+  }
 
   /** @private */
   updateCameraCount_() {
@@ -388,7 +409,7 @@
         .catch(e => {
           this.cameraCount_ = 0;
         });
-  },
+  }
 
   /** @private */
   onHasCameraCountChanged_() {
@@ -399,7 +420,7 @@
       this.state_ = PageState.SWITCHING_CAM_ENVIRONMENT_TO_USER;
       this.startScanning_();
     }
-  },
+  }
 
   /** private */
   startScanning_() {
@@ -422,9 +443,11 @@
         .then(stream => {
           this.stream_ = stream;
           if (this.stream_) {
-            const video = this.$$('#video');
-            video.srcObject = stream;
-            this.playVideo_();
+            const video = this.shadowRoot.querySelector('#video');
+            if (video) {
+              video.srcObject = stream;
+              this.playVideo_();
+            }
           }
           this.stopStream_(oldStream);
 
@@ -440,7 +463,7 @@
         .catch(e => {
           this.state_ = PageState.SCANNING_FAILURE;
         });
-  },
+  }
 
   /**
    * Continuously checks stream if it contains a QR code. If a QR code is
@@ -474,7 +497,7 @@
     } catch (error) {
       this.state_ = PageState.SCANNING_FAILURE;
     }
-  },
+  }
 
   /**
    * @param {ImageBitmap} frame
@@ -494,22 +517,28 @@
       return qrCodes[0].rawValue;
     }
     return null;
-  },
+  }
 
   /** @private */
   onActivationCodeChanged_() {
-    this.fire('activation-code-updated', {
-      activationCode: this.validateActivationCode_(this.activationCode) ?
-          this.activationCode :
-          null,
+    const event = new CustomEvent('activation-code-updated', {
+      bubbles: true,
+      composed: true,
+      detail: {
+        activationCode: this.validateActivationCode_(this.activationCode) ?
+            this.activationCode :
+            null,
+      },
     });
-  },
+
+    this.dispatchEvent(event);
+  }
 
   /** @private */
   clearQrCodeDetectorTimer_() {
     clearTimeout(this.qrCodeDetectorTimer_);
     this.qrCodeDetectorTimer_ = null;
-  },
+  }
 
   /**
    * Checks if |activationCode| matches or partially matches the correct format.
@@ -544,7 +573,7 @@
       return false;
     }
     return true;
-  },
+  }
 
   /** @private */
   onSwitchCameraButtonPressed_() {
@@ -554,7 +583,7 @@
       this.state_ = PageState.SWITCHING_CAM_ENVIRONMENT_TO_USER;
     }
     this.startScanning_();
-  },
+  }
 
   /** @private */
   onShowErrorChanged_() {
@@ -568,7 +597,7 @@
         this.state_ = PageState.SCANNING_INSTALL_FAILURE;
       }
     }
-  },
+  }
 
   /** @private */
   onStateChanged_() {
@@ -603,11 +632,14 @@
     if (this.state_ === PageState.SCANNING_SUCCESS) {
       this.isFromQrCode = true;
       this.qrCodeCameraA11yString_ = this.i18n('qrCodeA11YCameraScanSuccess');
-      this.fire('focus-default-button');
+      this.dispatchEvent(new CustomEvent('focus-default-button', {
+        bubbles: true,
+        composed: true,
+      }));
     }
 
     this.expanded_ = false;
-  },
+  }
 
   /**
    * @param {KeyboardEvent} e
@@ -615,7 +647,10 @@
    */
   onKeyDown_(e) {
     if (e.key === 'Enter') {
-      this.fire('forward-navigation-requested');
+      this.dispatchEvent(new CustomEvent('forward-navigation-requested', {
+        bubbles: true,
+        composed: true,
+      }));
     }
 
     // Prevents barcode detector video from closing if user tabs through
@@ -627,7 +662,7 @@
 
     this.state_ = PageState.MANUAL_ENTRY;
     e.stopPropagation();
-  },
+  }
 
   /**
    * @param {UiElement} uiElement
@@ -661,7 +696,7 @@
       case UiElement.SCAN_INSTALL_FAILURE:
         return state !== PageState.SCANNING_INSTALL_FAILURE;
     }
-  },
+  }
 
   /**
    * @param {UiElement} uiElement
@@ -680,7 +715,7 @@
       default:
         return false;
     }
-  },
+  }
 
   /**
    * @return {string}
@@ -697,7 +732,7 @@
       return this.i18n('scanQRCodeNoProfilesFound');
     }
     return this.i18n('scanQRCode');
-  },
+  }
 
   /**
    * @param {PageState} state
@@ -709,7 +744,7 @@
       return true;
     }
     return state === PageState.MANUAL_ENTRY_INSTALL_FAILURE;
-  },
+  }
 
   /**
    * @param {boolean} showBusy
@@ -724,7 +759,7 @@
     // Because this string contains '<' and '>' characters, we cannot use i18n
     // methods.
     return loadTimeData.getString('scanQrCodeInputSubtitle');
-  },
+  }
 
   /**
    * @return {string}
@@ -734,5 +769,7 @@
     // Because this string contains '<' and '>' characters, we cannot use i18n
     // methods.
     return loadTimeData.getString('scanQrCodeInputError');
-  },
-});
+  }
+}
+
+customElements.define(ActivationCodePageElement.is, ActivationCodePageElement);
diff --git a/ash/webui/common/resources/cellular_setup/activation_verification_page.js b/ash/webui/common/resources/cellular_setup/activation_verification_page.js
index e9bff5a..8ba4fcf 100644
--- a/ash/webui/common/resources/cellular_setup/activation_verification_page.js
+++ b/ash/webui/common/resources/cellular_setup/activation_verification_page.js
@@ -11,14 +11,30 @@
 import '//resources/polymer/v3_0/iron-media-query/iron-media-query.js';
 import 'chrome://resources/cros_components/lottie_renderer/lottie-renderer.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './activation_verification_page.html.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'activation-verification-page',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const ActivationVerificationPageElementBase =
+    mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
-});
+/** @polymer */
+class ActivationVerificationPageElement extends
+    ActivationVerificationPageElementBase {
+  static get is() {
+    return 'activation-verification-page';
+  }
+
+  static get template() {
+    return getTemplate();
+  }
+}
+
+customElements.define(
+    ActivationVerificationPageElement.is, ActivationVerificationPageElement);
diff --git a/ash/webui/common/resources/cellular_setup/base_page.js b/ash/webui/common/resources/cellular_setup/base_page.js
index 7e096d0..357704c 100644
--- a/ash/webui/common/resources/cellular_setup/base_page.js
+++ b/ash/webui/common/resources/cellular_setup/base_page.js
@@ -8,42 +8,55 @@
 import '//resources/cr_elements/cr_shared_vars.css.js';
 import '//resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './base_page.html.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'base-page',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const BasePageElementBase = mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
+/** @polymer */
+class BasePageElement extends BasePageElementBase {
+  static get is() {
+    return 'base-page';
+  }
 
-  properties: {
-    /**
-     * Main title for the page.
-     *
-     * @type {string}
-     */
-    title: String,
+  static get template() {
+    return getTemplate();
+  }
 
-    /**
-     * Message displayed under the main title.
-     *
-     * @type {string}
-     */
-    message: String,
+  static get properties() {
+    return {
+      /**
+       * Main title for the page.
+       *
+       * @type {string}
+       */
+      title: String,
 
-    /**
-     * Name for the cellular-setup iconset iron-icon displayed beside message.
-     *
-     * @type {string}
-     */
-    messageIcon: {
-      type: String,
-      value: '',
-    },
-  },
+      /**
+       * Message displayed under the main title.
+       *
+       * @type {string}
+       */
+      message: String,
+
+      /**
+       * Name for the cellular-setup iconset iron-icon displayed beside message.
+       *
+       * @type {string}
+       */
+      messageIcon: {
+        type: String,
+        value: '',
+      },
+    };
+  }
 
   /**
    * @returns {string}
@@ -51,7 +64,7 @@
    */
   getTitle_() {
     return this.title;
-  },
+  }
 
   /**
    * @returns {boolean}
@@ -59,7 +72,7 @@
    */
   isTitleShown_() {
     return !!this.title;
-  },
+  }
 
   /**
    * @returns {boolean}
@@ -67,5 +80,7 @@
    */
   isMessageIconShown_() {
     return !!this.messageIcon;
-  },
-});
+  }
+}
+
+customElements.define(BasePageElement.is, BasePageElement);
diff --git a/ash/webui/common/resources/cellular_setup/button_bar.js b/ash/webui/common/resources/cellular_setup/button_bar.js
index a8ec87f..eacd080 100644
--- a/ash/webui/common/resources/cellular_setup/button_bar.js
+++ b/ash/webui/common/resources/cellular_setup/button_bar.js
@@ -10,43 +10,54 @@
 
 import {assert, assertNotReached} from '//resources/ash/common/assert.js';
 import {focusWithoutInk} from '//resources/ash/common/focus_without_ink_js.js';
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './button_bar.html.js';
 import {Button, ButtonBarState, ButtonState, CellularSetupPageName} from './cellular_types.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'button-bar',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const ButtonBarElementBase = mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [
-    I18nBehavior,
-  ],
+/** @polymer */
+export class ButtonBarElement extends ButtonBarElementBase {
+  static get is() {
+    return 'button-bar';
+  }
 
-  properties: {
-    /**
-     * Sets the states of all buttons
-     * @type {!ButtonBarState}
-     */
-    buttonState: {
-      type: Object,
-      value: {},
-    },
+  static get template() {
+    return getTemplate();
+  }
 
-    /**
-     * @type {!Button}
-     */
-    Button: {
-      type: Object,
-      value: Button,
-    },
+  static get properties() {
+    return {
+      /**
+       * Sets the states of all buttons
+       * @type {!ButtonBarState}
+       */
+      buttonState: {
+        type: Object,
+        value: {},
+      },
 
-    forwardButtonLabel: {
-      type: String,
-      value: '',
-    },
-  },
+      /**
+       * @type {!Button}
+       */
+      Button: {
+        type: Object,
+        value: Button,
+      },
+
+      forwardButtonLabel: {
+        type: String,
+        value: '',
+      },
+    };
+  }
 
   /**
    * @param {!Button} buttonName
@@ -56,7 +67,7 @@
   isButtonHidden_(buttonName) {
     const state = this.getButtonBarState_(buttonName);
     return state === ButtonState.HIDDEN;
-  },
+  }
 
   /**
    * @param {!Button} buttonName
@@ -66,7 +77,7 @@
   isButtonDisabled_(buttonName) {
     const state = this.getButtonBarState_(buttonName);
     return state === ButtonState.DISABLED;
-  },
+  }
 
   focusDefaultButton() {
     const buttons = this.shadowRoot.querySelectorAll('cr-button');
@@ -78,22 +89,31 @@
         return;
       }
     }
-  },
+  }
 
   /** @private */
   onBackwardButtonClicked_() {
-    this.fire('backward-nav-requested');
-  },
+    this.dispatchEvent(new CustomEvent('backward-nav-requested', {
+      bubbles: true,
+      composed: true,
+    }));
+  }
 
   /** @private */
   onCancelButtonClicked_() {
-    this.fire('cancel-requested');
-  },
+    this.dispatchEvent(new CustomEvent('cancel-requested', {
+      bubbles: true,
+      composed: true,
+    }));
+  }
 
   /** @private */
   onForwardButtonClicked_() {
-    this.fire('forward-nav-requested');
-  },
+    this.dispatchEvent(new CustomEvent('forward-nav-requested', {
+      bubbles: true,
+      composed: true,
+    }));
+  }
 
   /**
    * @param {!Button} button
@@ -113,5 +133,7 @@
         assertNotReached();
         return ButtonState.ENABLED;
     }
-  },
-});
+  }
+}
+
+customElements.define(ButtonBarElement.is, ButtonBarElement);
diff --git a/ash/webui/common/resources/cellular_setup/cellular_setup.js b/ash/webui/common/resources/cellular_setup/cellular_setup.js
index d2b4c7d..9ae8b0a 100644
--- a/ash/webui/common/resources/cellular_setup/cellular_setup.js
+++ b/ash/webui/common/resources/cellular_setup/cellular_setup.js
@@ -10,102 +10,123 @@
 import './psim_flow_ui.js';
 import './esim_flow_ui.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './cellular_setup.html.js';
 import {CellularSetupDelegate} from './cellular_setup_delegate.js';
 import {ButtonBarState, CellularSetupPageName} from './cellular_types.js';
+import {EsimFlowUiElement} from './esim_flow_ui.js';
+import {PsimFlowUiElement} from './psim_flow_ui.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'cellular-setup',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const CellularSetupElementBase = mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
+/** @polymer */
+class CellularSetupElement extends CellularSetupElementBase {
+  static get is() {
+    return 'cellular-setup';
+  }
 
-  properties: {
-    /** @type {!CellularSetupDelegate} */
-    delegate: Object,
+  static get template() {
+    return getTemplate();
+  }
 
-    /**
-     * Banner used in pSIM flow to show carrier network name. No banner
-     * shown if the string is empty.
-     */
-    flowPsimBanner: {
-      type: String,
-      notify: true,
-      value: '',
-    },
+  static get properties() {
+    return {
+      /** @type {!CellularSetupDelegate} */
+      delegate: Object,
 
-    /**
-     * Header for the flow, shown below the title. No header shown if the string
-     * is empty.
-     */
-    flowHeader: {
-      type: String,
-      notify: true,
-      value: '',
-    },
+      /**
+       * Banner used in pSIM flow to show carrier network name. No banner
+       * shown if the string is empty.
+       */
+      flowPsimBanner: {
+        type: String,
+        notify: true,
+        value: '',
+      },
 
-    /**
-     * Name of the currently displayed sub-page.
-     * @private {!CellularSetupPageName|null}
-     */
-    currentPageName: String,
+      /**
+       * Header for the flow, shown below the title. No header shown if the
+       * string is empty.
+       */
+      flowHeader: {
+        type: String,
+        notify: true,
+        value: '',
+      },
 
-    /**
-     * Current user selected setup flow page name.
-     * @private {!CellularSetupPageName|null}
-     */
-    selectedFlow_: {
-      type: String,
-      value: null,
-    },
+      /**
+       * Name of the currently displayed sub-page.
+       * @private {!CellularSetupPageName|null}
+       */
+      currentPageName: String,
 
-    /**
-     * Button bar button state.
-     * @private {!ButtonBarState}
-     */
-    buttonState_: {
-      type: Object,
-      notify: true,
-    },
+      /**
+       * Current user selected setup flow page name.
+       * @private {!CellularSetupPageName|null}
+       */
+      selectedFlow_: {
+        type: String,
+        value: null,
+      },
 
-    /**
-     * DOM Element corresponding to the visible page.
-     *
-     * @private {!PsimFlowUiElement|!EsimFlowUiElement}
-     */
-    currentPage_: {
-      type: Object,
-      observer: 'onPageChange_',
-    },
+      /**
+       * Button bar button state.
+       * @private {!ButtonBarState}
+       */
+      buttonState_: {
+        type: Object,
+        notify: true,
+      },
 
-    /**
-     * Text for the button_bar's 'Forward' button.
-     * @private {string}
-     */
-    forwardButtonLabel_: {
-      type: String,
-    },
-  },
+      /**
+       * DOM Element corresponding to the visible page.
+       *
+       * @private {!PsimFlowUiElement|!EsimFlowUiElement}
+       */
+      currentPage_: {
+        type: Object,
+        observer: 'onPageChange_',
+      },
 
-  listeners: {
-    'backward-nav-requested': 'onBackwardNavRequested_',
-    'retry-requested': 'onRetryRequested_',
-    'forward-nav-requested': 'onForwardNavRequested_',
-    'cancel-requested': 'onCancelRequested_',
-    'focus-default-button': 'onFocusDefaultButton_',
-  },
+      /**
+       * Text for the button_bar's 'Forward' button.
+       * @private {string}
+       */
+      forwardButtonLabel_: {
+        type: String,
+      },
 
+    };
+  }
 
   /** @override */
-  attached() {
+  connectedCallback() {
+    super.connectedCallback();
+
     // By default eSIM flow is selected.
     if (!this.currentPageName) {
       this.currentPageName = CellularSetupPageName.ESIM_FLOW_UI;
     }
-  },
+  }
+
+  /** override */
+  ready() {
+    super.ready();
+
+    this.addEventListener(
+        'backward-nav-requested', this.onBackwardNavRequested_);
+    this.addEventListener('retry-requested', this.onRetryRequested_);
+    this.addEventListener('forward-nav-requested', this.onForwardNavRequested_);
+    this.addEventListener('cancel-requested', this.onCancelRequested_);
+    this.addEventListener('focus-default-button', this.onFocusDefaultButton_);
+  }
 
   /** @private */
   onPageChange_() {
@@ -113,31 +134,34 @@
       this.flowPsimBanner = '';
       this.currentPage_.initSubflow();
     }
-  },
+  }
 
   /** @private */
   onBackwardNavRequested_() {
     this.currentPage_.navigateBackward();
-  },
+  }
 
   onCancelRequested_() {
-    this.fire('exit-cellular-setup');
-  },
+    this.dispatchEvent(new CustomEvent('exit-cellular-setup', {
+      bubbles: true,
+      composed: true,
+    }));
+  }
 
   /** @private */
   onRetryRequested_() {
     // TODO(crbug.com/1093185): Add try again logic.
-  },
+  }
 
   /** @private */
   onForwardNavRequested_() {
     this.currentPage_.navigateForward();
-  },
+  }
 
   /** @private */
   onFocusDefaultButton_() {
     this.$.buttonBar.focusDefaultButton();
-  },
+  }
 
   /**
    * @param {string} currentPage
@@ -145,7 +169,7 @@
    */
   shouldShowPsimFlow_(currentPage) {
     return currentPage === CellularSetupPageName.PSIM_FLOW_UI;
-  },
+  }
 
   /**
    * @param {string} currentPage
@@ -153,5 +177,7 @@
    */
   shouldShowEsimFlow_(currentPage) {
     return currentPage === CellularSetupPageName.ESIM_FLOW_UI;
-  },
-});
+  }
+}
+
+customElements.define(CellularSetupElement.is, CellularSetupElement);
diff --git a/ash/webui/common/resources/cellular_setup/confirmation_code_page.js b/ash/webui/common/resources/cellular_setup/confirmation_code_page.js
index c14b821e..337e3a9 100644
--- a/ash/webui/common/resources/cellular_setup/confirmation_code_page.js
+++ b/ash/webui/common/resources/cellular_setup/confirmation_code_page.js
@@ -12,36 +12,46 @@
 import '//resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
 import './base_page.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {mojoString16ToString} from 'chrome://resources/js/mojo_type_util.js';
 import {ESimProfileProperties} from 'chrome://resources/mojo/chromeos/ash/services/cellular_setup/public/mojom/esim_manager.mojom-webui.js';
 
 import {getTemplate} from './confirmation_code_page.html.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'confirmation-code-page',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const ConfirmationCodePageElementBase =
+    mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
+/** @polymer */
+class ConfirmationCodePageElement extends ConfirmationCodePageElementBase {
+  static get is() {
+    return 'confirmation-code-page';
+  }
 
-  properties: {
-    /**
-     * @type {?ESimProfileProperties}
-     */
-    profileProperties: {
-      type: Object,
-    },
+  static get template() {
+    return getTemplate();
+  }
 
-    confirmationCode: {
-      type: String,
-      notify: true,
-    },
+  static get properties() {
+    return {
+      /**
+       * @type {?ESimProfileProperties}
+       */
+      profileProperties: Object,
 
-    showError: {
-      type: Boolean,
-    },
-  },
+      confirmationCode: {
+        type: String,
+        notify: true,
+      },
+
+      showError: Boolean,
+    };
+  }
 
   /**
    * @param {KeyboardEvent} e
@@ -49,10 +59,13 @@
    */
   onKeyDown_(e) {
     if (e.key === 'Enter') {
-      this.fire('forward-navigation-requested');
+      this.dispatchEvent(new CustomEvent('forward-navigation-requested', {
+        bubbles: true,
+        composed: true,
+      }));
     }
     e.stopPropagation();
-  },
+  }
 
   /**
    * @return {string}
@@ -63,5 +76,8 @@
       return '';
     }
     return mojoString16ToString(this.profileProperties.name);
-  },
-});
+  }
+}
+
+customElements.define(
+    ConfirmationCodePageElement.is, ConfirmationCodePageElement);
diff --git a/ash/webui/common/resources/cellular_setup/confirmation_code_page_legacy.js b/ash/webui/common/resources/cellular_setup/confirmation_code_page_legacy.js
index 6da4963d..b4dd1e6 100644
--- a/ash/webui/common/resources/cellular_setup/confirmation_code_page_legacy.js
+++ b/ash/webui/common/resources/cellular_setup/confirmation_code_page_legacy.js
@@ -12,63 +12,78 @@
 import '//resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
 import './base_page.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {mojoString16ToString} from 'chrome://resources/js/mojo_type_util.js';
 import {ESimProfileProperties, ESimProfileRemote} from 'chrome://resources/mojo/chromeos/ash/services/cellular_setup/public/mojom/esim_manager.mojom-webui.js';
 
 import {getTemplate} from './confirmation_code_page_legacy.html.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'confirmation-code-page-legacy',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const ConfirmationCodePageLegacyElementBase =
+    mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
+/** @polymer */
+class ConfirmationCodePageLegacyElement extends
+    ConfirmationCodePageLegacyElementBase {
+  static get is() {
+    return 'confirmation-code-page-legacy';
+  }
 
-  properties: {
-    /**
-     * @type {?ESimProfileRemote}
-     */
-    profile: {
-      type: Object,
-      observer: 'onProfileChanged_',
-    },
+  static get template() {
+    return getTemplate();
+  }
 
-    confirmationCode: {
-      type: String,
-      notify: true,
-    },
+  static get properties() {
+    return {
+      /**
+       * @type {?ESimProfileRemote}
+       */
+      profile: {
+        type: Object,
+        observer: 'onProfileChanged_',
+      },
 
-    showError: {
-      type: Boolean,
-    },
+      confirmationCode: {
+        type: String,
+        notify: true,
+      },
 
-    /**
-     * Indicates the UI is busy with an operation and cannot be interacted with.
-     */
-    showBusy: {
-      type: Boolean,
-      value: false,
-    },
+      showError: Boolean,
 
-    /**
-     * @type {?ESimProfileProperties}
-     * @private
-     */
-    profileProperties_: {
-      type: Object,
-      value: null,
-    },
+      /**
+       * Indicates the UI is busy with an operation and cannot be interacted
+       * with.
+       */
+      showBusy: {
+        type: Boolean,
+        value: false,
+      },
 
-    /**
-     * @type {boolean}
-     * @private
-     */
-    isDarkModeActive_: {
-      type: Boolean,
-      value: false,
-    },
-  },
+      /**
+       * @type {?ESimProfileProperties}
+       * @private
+       */
+      profileProperties_: {
+        type: Object,
+        value: null,
+      },
+
+      /**
+       * @type {boolean}
+       * @private
+       */
+      isDarkModeActive_: {
+        type: Boolean,
+        value: false,
+      },
+
+    };
+  }
 
   /** @private */
   onProfileChanged_() {
@@ -79,7 +94,7 @@
     this.profile.getProperties().then(response => {
       this.profileProperties_ = response.properties;
     });
-  },
+  }
 
   /**
    * @param {KeyboardEvent} e
@@ -87,10 +102,13 @@
    */
   onKeyDown_(e) {
     if (e.key === 'Enter') {
-      this.fire('forward-navigation-requested');
+      this.dispatchEvent(new CustomEvent('forward-navigation-requested', {
+        bubbles: true,
+        composed: true,
+      }));
     }
     e.stopPropagation();
-  },
+  }
 
   /**
    * @return {boolean}
@@ -98,7 +116,7 @@
    */
   shouldShowProfileDetails_() {
     return !!this.profile;
-  },
+  }
 
   /**
    * @return {string}
@@ -109,7 +127,7 @@
       return '';
     }
     return mojoString16ToString(this.profileProperties_.name);
-  },
+  }
 
   /**
    * @return {string}
@@ -119,5 +137,8 @@
     return this.isDarkModeActive_ ?
         'chrome://resources/ash/common/cellular_setup/default_esim_profile_dark.svg' :
         'chrome://resources/ash/common/cellular_setup/default_esim_profile.svg';
-  },
-});
+  }
+}
+
+customElements.define(
+    ConfirmationCodePageLegacyElement.is, ConfirmationCodePageLegacyElement);
diff --git a/ash/webui/common/resources/cellular_setup/esim_flow_ui.js b/ash/webui/common/resources/cellular_setup/esim_flow_ui.js
index 5d0966c..004aa18 100644
--- a/ash/webui/common/resources/cellular_setup/esim_flow_ui.js
+++ b/ash/webui/common/resources/cellular_setup/esim_flow_ui.js
@@ -14,11 +14,11 @@
 import './confirmation_code_page.js';
 
 import {assert, assertNotReached} from '//resources/ash/common/assert.js';
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
 import {hasActiveCellularNetwork} from '//resources/ash/common/network/cellular_utils.js';
 import {MojoInterfaceProvider, MojoInterfaceProviderImpl} from '//resources/ash/common/network/mojo_interface_provider.js';
-import {NetworkListenerBehavior} from '//resources/ash/common/network/network_listener_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {NetworkListenerBehavior, NetworkListenerBehaviorInterface} from '//resources/ash/common/network/network_listener_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
 import {ESimManagerRemote, ESimOperationResult, ESimProfileProperties, ESimProfileRemote, EuiccRemote, ProfileInstallMethod, ProfileInstallResult, ProfileState} from 'chrome://resources/mojo/chromeos/ash/services/cellular_setup/public/mojom/esim_manager.mojom-webui.js';
 import {FilterType, NetworkStateProperties, NO_LIMIT} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
@@ -89,197 +89,190 @@
  * Root element for the eSIM cellular setup flow. This element interacts with
  * the CellularSetup service to carry out the esim activation flow.
  */
-Polymer({
-  _template: getTemplate(),
-  is: 'esim-flow-ui',
 
-  behaviors: [
-    I18nBehavior,
-    NetworkListenerBehavior,
-    SubflowBehavior,
-  ],
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ * @implements {NetworkListenerBehaviorInterface}
+ */
+const EsimFlowUiElementBase = mixinBehaviors(
+    [I18nBehavior, NetworkListenerBehavior, SubflowBehavior], PolymerElement);
 
-  properties: {
-    /** @type {!CellularSetupDelegate} */
-    delegate: Object,
+/** @polymer */
+export class EsimFlowUiElement extends EsimFlowUiElementBase {
+  static get is() {
+    return 'esim-flow-ui';
+  }
 
-    /**
-     * Header shown at the top of the flow. No header shown if the string is
-     * empty.
-     */
-    header: {
-      type: String,
-      notify: true,
-      computed: 'computeHeader_(selectedESimPageName_, showError_)',
-    },
+  static get template() {
+    return getTemplate();
+  }
 
-    forwardButtonLabel: {
-      type: String,
-      notify: true,
-    },
+  static get properties() {
+    return {
+      /** @type {!CellularSetupDelegate} */
+      delegate: Object,
 
-    /**
-     * @type {!ESimUiState}
-     * @private
-     */
-    state_: {
-      type: String,
-      value: function() {
-        if (loadTimeData.valueExists('isSmdsSupportEnabled') &&
-            loadTimeData.getBoolean('isSmdsSupportEnabled')) {
-          return ESimUiState.PROFILE_SEARCH_CONSENT;
-        }
-        return ESimUiState.PROFILE_SEARCH;
+      /**
+       * Header shown at the top of the flow. No header shown if the string is
+       * empty.
+       */
+      header: {
+        type: String,
+        notify: true,
+        computed: 'computeHeader_(selectedESimPageName_, showError_)',
       },
-      observer: 'onStateChanged_',
-    },
 
-    /**
-     * Element name of the current selected sub-page.
-     * This is set in updateSelectedPage_ on initialization.
-     * @type {?ESimPageName}
-     * @private
-     */
-    selectedESimPageName_: String,
-
-    /**
-     * Whether the user has consented to a scan for profiles.
-     * @type {boolean}
-     */
-    hasConsentedForDiscovery_: {
-      type: Boolean,
-      value: false,
-    },
-
-    /**
-     * Whether the user is setting up the eSIM profile manually.
-     * @type {boolean}
-     */
-    shouldSkipDiscovery_: {
-      type: Boolean,
-      value: false,
-    },
-
-    /**
-     * Whether error state should be shown for the current page.
-     * @private {boolean}
-     */
-    showError_: {
-      type: Boolean,
-      value: false,
-    },
-
-    /**
-     * Profiles fetched that have status kPending.
-     * @type {!Array<!ESimProfileRemote>}
-     * @private
-     */
-    pendingProfiles_: {
-      type: Array,
-    },
-
-    /**
-     * Profile selected to be installed.
-     * @type {?ESimProfileRemote}
-     * @private
-     */
-    selectedProfile_: {
-      type: Object,
-    },
-
-    /**
-     * Profile properties fetched from the latest SM-DS scan.
-     * @type {!Array<!ESimProfileProperties>}
-     * @private
-     */
-    pendingProfileProperties_: {
-      type: Array,
-    },
-
-    /**
-     * Profile properties selected to be installed.
-     * @type {?ESimProfileProperties}
-     * @private
-     */
-    selectedProfileProperties_: {
-      type: Object,
-    },
-
-    /** @private */
-    activationCode_: {
-      type: String,
-      value: '',
-    },
-
-    /** @private */
-    confirmationCode_: {
-      type: String,
-      value: '',
-      observer: 'onConfirmationCodeUpdated_',
-    },
-
-    /** @private */
-    hasHadActiveCellularNetwork_: {
-      type: Boolean,
-      value: false,
-    },
-
-    /** @private */
-    isActivationCodeFromQrCode_: {
-      type: Boolean,
-    },
-
-    /**
-     * Return true if SmdsSupportEnabled feature flag is enabled.
-     */
-    smdsSupportEnabled_: {
-      type: Boolean,
-      value() {
-        return loadTimeData.valueExists('isSmdsSupportEnabled') &&
-            loadTimeData.getBoolean('isSmdsSupportEnabled');
+      forwardButtonLabel: {
+        type: String,
+        notify: true,
       },
-    },
-  },
 
-  /**
-   * Provides an interface to the ESimManager Mojo service.
-   * @private {?ESimManagerRemote}
-   */
-  eSimManagerRemote_: null,
+      /**
+       * @type {!ESimUiState}
+       * @private
+       */
+      state_: {
+        type: String,
+        value: function() {
+          if (loadTimeData.valueExists('isSmdsSupportEnabled') &&
+              loadTimeData.getBoolean('isSmdsSupportEnabled')) {
+            return ESimUiState.PROFILE_SEARCH_CONSENT;
+          }
+          return ESimUiState.PROFILE_SEARCH;
+        },
+        observer: 'onStateChanged_',
+      },
 
-  /** @private {?EuiccRemote} */
-  euicc_: null,
+      /**
+       * Element name of the current selected sub-page.
+       * This is set in updateSelectedPage_ on initialization.
+       * @type {?ESimPageName}
+       * @private
+       */
+      selectedESimPageName_: String,
 
-  /** @private {boolean} */
-  hasFailedFetchingProfiles_: false,
+      /**
+       * Whether the user has consented to a scan for profiles.
+       * @type {boolean}
+       */
+      hasConsentedForDiscovery_: {
+        type: Boolean,
+        value: false,
+      },
 
-  /** @private {?ProfileInstallResult} */
-  lastProfileInstallResult_: null,
+      /**
+       * Whether the user is setting up the eSIM profile manually.
+       * @type {boolean}
+       */
+      shouldSkipDiscovery_: {
+        type: Boolean,
+        value: false,
+      },
 
-  /**
-   * If there are no active network connections of any type.
-   * @private {boolean}
-   */
-  isOffline_: false,
+      /**
+       * Whether error state should be shown for the current page.
+       * @private {boolean}
+       */
+      showError_: {
+        type: Boolean,
+        value: false,
+      },
 
-  /**
-   * The time at which the ESim flow is attached.
-   * @private {?Date}
-   */
-  timeOnAttached_: null,
+      /**
+       * Profiles fetched that have status kPending.
+       * @type {!Array<!ESimProfileRemote>}
+       * @private
+       */
+      pendingProfiles_: Array,
 
-  listeners: {
-    'activation-code-updated': 'onActivationCodeUpdated_',
-    'forward-navigation-requested': 'onForwardNavigationRequested_',
-  },
+      /**
+       * Profile selected to be installed.
+       * @type {?ESimProfileRemote}
+       * @private
+       */
+      selectedProfile_: {
+        type: Object,
+        observer: 'onSelectedProfileChanged_',
+      },
 
-  observers: [
-    'onSelectedProfileChanged_(selectedProfile_)',
-    'onSelectedProfilePropertiesChanged_(selectedProfileProperties_)',
-  ],
+      /**
+       * Profile properties fetched from the latest SM-DS scan.
+       * @type {!Array<!ESimProfileProperties>}
+       * @private
+       */
+      pendingProfileProperties_: Array,
+
+      /**
+       * Profile properties selected to be installed.
+       * @type {?ESimProfileProperties}
+       * @private
+       */
+      selectedProfileProperties_: {
+        type: Object,
+        observer: 'onSelectedProfilePropertiesChanged_',
+      },
+
+      /** @private */
+      activationCode_: {
+        type: String,
+        value: '',
+      },
+
+      /** @private */
+      confirmationCode_: {
+        type: String,
+        value: '',
+        observer: 'onConfirmationCodeUpdated_',
+      },
+
+      /** @private */
+      hasHadActiveCellularNetwork_: {
+        type: Boolean,
+        value: false,
+      },
+
+      /** @private */
+      isActivationCodeFromQrCode_: Boolean,
+
+      /**
+       * Return true if SmdsSupportEnabled feature flag is enabled.
+       */
+      smdsSupportEnabled_: {
+        type: Boolean,
+        value() {
+          return loadTimeData.valueExists('isSmdsSupportEnabled') &&
+              loadTimeData.getBoolean('isSmdsSupportEnabled');
+        },
+      },
+
+    };
+  }
 
   /** @override */
-  created() {
+  constructor() {
+    super();
+
+    /** @private {?EuiccRemote} */
+    this.euicc_ = null;
+
+    /** @private {boolean} */
+    this.hasFailedFetchingProfiles_ = false;
+
+    /** @private {?ProfileInstallResult} */
+    this.lastProfileInstallResult_ = null;
+
+    /**
+     * If there are no active network connections of any type.
+     * @private {boolean}
+     */
+    this.isOffline_ = false;
+
+    /**
+     * Provides an interface to the ESimManager Mojo service.
+     * @private {?ESimManagerRemote}
+     */
     this.eSimManagerRemote_ = getESimManagerRemote();
     const networkConfig =
         MojoInterfaceProviderImpl.getInstance().getMojoServiceRemote();
@@ -292,18 +285,36 @@
     networkConfig.getNetworkStateList(filter).then(response => {
       this.onActiveNetworksChanged(response.result);
     });
-  },
+  }
 
   /** @override */
-  attached() {
+  connectedCallback() {
+    super.connectedCallback();
+
+    /**
+     * The time at which the ESim flow is attached.
+     * @private {?Date}
+     */
     this.timeOnAttached_ = new Date();
-  },
+  }
 
   /** @override */
-  detached() {
+  disconnectedCallback() {
+    super.disconnectedCallback();
+
     let resultCode = null;
 
     switch (this.lastProfileInstallResult_) {
+      case null:
+        // Handles case when no profile installation was attempted.
+        if (this.hasFailedFetchingProfiles_) {
+          resultCode = ESimSetupFlowResult.ERROR_FETCHING_PROFILES;
+        } else if (this.noProfilesFound_()) {
+          resultCode = ESimSetupFlowResult.CANCELLED_NO_PROFILES;
+        } else {
+          resultCode = ESimSetupFlowResult.CANCELLED_WITHOUT_ERROR;
+        }
+        break;
       case ProfileInstallResult.kSuccess:
         resultCode = ESimSetupFlowResult.SUCCESS;
         break;
@@ -317,14 +328,6 @@
         resultCode = ESimSetupFlowResult.CANCELLED_INVALID_ACTIVATION_CODE;
         break;
       default:
-        // Handles case when no profile installation was attempted.
-        if (this.hasFailedFetchingProfiles_) {
-          resultCode = ESimSetupFlowResult.ERROR_FETCHING_PROFILES;
-        } else if (this.noProfilesFound_()) {
-          resultCode = ESimSetupFlowResult.CANCELLED_NO_PROFILES;
-        } else {
-          resultCode = ESimSetupFlowResult.CANCELLED_WITHOUT_ERROR;
-        }
         break;
     }
 
@@ -346,7 +349,18 @@
 
     chrome.metricsPrivate.recordLongTime(
         FAILED_ESIM_SETUP_DURATION_METRIC_NAME, elapsedTimeMs);
-  },
+  }
+
+  /** override */
+  ready() {
+    super.ready();
+
+    this.addEventListener('activation-code-updated', (event) => {
+      this.onActivationCodeUpdated_(event);
+    });
+    this.addEventListener(
+        'forward-navigation-requested', this.onForwardNavigationRequested_);
+  }
 
   /**
    * NetworkListenerBehavior override
@@ -357,7 +371,7 @@
   onActiveNetworksChanged(activeNetworks) {
     this.isOffline_ = !activeNetworks.some(
         (network) => network.connectionState === ConnectionStateType.kOnline);
-  },
+  }
 
   initSubflow() {
     if (!this.smdsSupportEnabled_) {
@@ -366,7 +380,7 @@
       this.getEuicc_();
     }
     this.onNetworkStateListChanged();
-  },
+  }
 
   /** @private */
   async fetchProfiles_() {
@@ -385,7 +399,7 @@
     } else {
       this.state_ = ESimUiState.PROFILE_SELECTION;
     }
-  },
+  }
 
   /** @private */
   async getEuicc_() {
@@ -398,7 +412,7 @@
       return;
     }
     this.euicc_ = euicc;
-  },
+  }
 
   /**
    * @private
@@ -419,7 +433,7 @@
           return properties.state === ProfileState.kPending &&
               properties.activationCode;
         });
-  },
+  }
 
   /**
    * @private
@@ -436,7 +450,7 @@
       this.pendingProfiles_ = [];
     }
     this.pendingProfiles_ = await getPendingESimProfiles(this.euicc_);
-  },
+  }
 
   /**
    * @private
@@ -464,7 +478,7 @@
         response.result === ProfileInstallResult.kFailure) {
       this.state_ = ESimUiState.SETUP_FINISH;
     }
-  },
+  }
 
   /** @private */
   onStateChanged_(newState, oldState) {
@@ -475,7 +489,7 @@
       this.fetchProfiles_();
     }
     this.initializePageState_(newState, oldState);
-  },
+  }
 
   /** @private */
   updateSelectedPage_() {
@@ -532,9 +546,12 @@
     }
     // If there is a page change, fire focus event.
     if (oldSelectedESimPageName !== this.selectedESimPageName_) {
-      this.fire('focus-default-button');
+      this.dispatchEvent(new CustomEvent('focus-default-button', {
+        bubbles: true,
+        composed: true,
+      }));
     }
-  },
+  }
 
   /**
    * @param {boolean} enableForwardBtn
@@ -555,7 +572,7 @@
       cancel: cancelButtonStateIfEnabled,
       forward: enableForwardBtn ? ButtonState.ENABLED : ButtonState.DISABLED,
     };
-  },
+  }
 
   /**
    * @param {boolean} enableForwardBtn
@@ -576,7 +593,7 @@
       cancel: cancelButtonStateIfEnabled,
       forward: enableForwardBtn ? ButtonState.ENABLED : ButtonState.DISABLED,
     };
-  },
+  }
 
   /** @private */
   updateButtonBarState_() {
@@ -662,7 +679,7 @@
         break;
     }
     this.set('buttonState', buttonState);
-  },
+  }
 
   /** @private */
   updateForwardButtonLabel_() {
@@ -675,7 +692,7 @@
           this.i18n('next') :
           this.i18n('skipDiscovery');
     }
-  },
+  }
 
   /** @private */
   initializePageState_(newState, oldState) {
@@ -687,7 +704,7 @@
         oldState !== ESimUiState.ACTIVATION_CODE_ENTRY_READY) {
       this.activationCode_ = '';
     }
-  },
+  }
 
   /** @private */
   onActivationCodeUpdated_(event) {
@@ -701,7 +718,7 @@
     this.state_ = event.detail.activationCode ?
         ESimUiState.ACTIVATION_CODE_ENTRY_READY :
         ESimUiState.ACTIVATION_CODE_ENTRY;
-  },
+  }
 
   /** @private */
   onSelectedProfileChanged_() {
@@ -715,7 +732,7 @@
       return;
     }
     this.updateForwardButtonLabel_();
-  },
+  }
 
   /** @private */
   onSelectedProfilePropertiesChanged_() {
@@ -729,7 +746,7 @@
       return;
     }
     this.updateForwardButtonLabel_();
-  },
+  }
 
   /** @private */
   onConfirmationCodeUpdated_() {
@@ -743,7 +760,7 @@
     this.state_ = this.confirmationCode_ ?
         ESimUiState.CONFIRMATION_CODE_ENTRY_READY :
         ESimUiState.CONFIRMATION_CODE_ENTRY;
-  },
+  }
 
   /** SubflowBehavior override */
   navigateForward() {
@@ -824,13 +841,16 @@
         }
         break;
       case ESimUiState.SETUP_FINISH:
-        this.fire('exit-cellular-setup');
+        this.dispatchEvent(new CustomEvent('exit-cellular-setup', {
+          bubbles: true,
+          composed: true,
+        }));
         break;
       default:
         assertNotReached();
         break;
     }
-  },
+  }
 
   /** SubflowBehavior override */
   navigateBackward() {
@@ -855,7 +875,7 @@
         'Navigate backward faled for : ' + this.state_ +
         ' this state does not support backward navigation.');
     assertNotReached();
-  },
+  }
 
   /** @private */
   onForwardNavigationRequested_() {
@@ -865,7 +885,7 @@
         this.state_ === ESimUiState.PROFILE_SELECTION) {
       this.navigateForward();
     }
-  },
+  }
 
   /** NetworkListenerBehavior override */
   onNetworkStateListChanged() {
@@ -877,14 +897,14 @@
         this.hasHadActiveCellularNetwork_ = hasActive;
       }
     });
-  },
+  }
 
   /** @private */
   shouldShowSubpageBusy_() {
     return this.state_ === ESimUiState.ACTIVATION_CODE_ENTRY_INSTALLING ||
         this.state_ === ESimUiState.CONFIRMATION_CODE_ENTRY_INSTALLING ||
         this.state_ === ESimUiState.PROFILE_SELECTION_INSTALLING;
-  },
+  }
 
   /** @private */
   getLoadingMessage_() {
@@ -895,7 +915,7 @@
     return this.hasHadActiveCellularNetwork_ ?
         this.i18n('eSimProfileDetectDuringActiveCellularConnectionMessage') :
         this.i18n('eSimProfileDetectMessage');
-  },
+  }
 
   /**
    * @return {string}
@@ -924,7 +944,7 @@
     }
 
     return '';
-  },
+  }
 
   /**
    * @return {ProfileInstallMethod}
@@ -939,7 +959,7 @@
     return this.hasConsentedForDiscovery_ ?
         ProfileInstallMethod.kViaActivationCodeAfterSmds :
         ProfileInstallMethod.kViaActivationCodeSkippedSmds;
-  },
+  }
 
   /**
    * Returns true if profiles have been received and none were found.
@@ -954,7 +974,7 @@
     } else {
       return (this.pendingProfiles_ && this.pendingProfiles_.length === 0);
     }
-  },
+  }
 
   /** @private*/
   profilesFound_() {
@@ -965,5 +985,7 @@
     } else {
       return (this.pendingProfiles_ && this.pendingProfiles_.length > 0);
     }
-  },
-});
+  }
+}
+
+customElements.define(EsimFlowUiElement.is, EsimFlowUiElement);
diff --git a/ash/webui/common/resources/cellular_setup/final_page.js b/ash/webui/common/resources/cellular_setup/final_page.js
index e22b03a..176c236 100644
--- a/ash/webui/common/resources/cellular_setup/final_page.js
+++ b/ash/webui/common/resources/cellular_setup/final_page.js
@@ -9,34 +9,48 @@
  */
 import './base_page.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {CellularSetupDelegate} from './cellular_setup_delegate.js';
 import {getTemplate} from './final_page.html.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'final-page',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const FinalPageElementBase = mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
+/** @polymer */
+export class FinalPageElement extends FinalPageElementBase {
+  static get is() {
+    return 'final-page';
+  }
 
-  properties: {
-    /** @type {!CellularSetupDelegate} */
-    delegate: Object,
+  static get template() {
+    return getTemplate();
+  }
 
-    /**
-     * Whether error state should be shown.
-     * @type {boolean}
-     */
-    showError: Boolean,
+  static get properties() {
+    return {
+      /** @type {!CellularSetupDelegate} */
+      delegate: Object,
 
-    /** @type {string} */
-    message: String,
+      /**
+       * Whether error state should be shown.
+       * @type {boolean}
+       */
+      showError: Boolean,
 
-    /** @type {string} */
-    errorMessage: String,
-  },
+      /** @type {string} */
+      message: String,
+
+      /** @type {string} */
+      errorMessage: String,
+
+    };
+  }
 
   /**
    * @param {boolean} showError
@@ -49,7 +63,7 @@
                          this.i18n('finalPageTitle');
     }
     return null;
-  },
+  }
 
   /**
    * @param {boolean} showError
@@ -58,7 +72,7 @@
    */
   getMessage_(showError) {
     return showError ? this.errorMessage : this.message;
-  },
+  }
 
   /**
    * @param {boolean} showError
@@ -67,7 +81,7 @@
    */
   getPageBodyClass_(showError) {
     return showError ? 'error' : '';
-  },
+  }
 
   /**
    * @param {boolean} showError
@@ -77,5 +91,7 @@
   getJellyIllustrationName_(showError) {
     return showError ? 'cellular-setup-illo:error' :
                        'cellular-setup-illo:final-page-success';
-  },
-});
+  }
+}
+
+customElements.define(FinalPageElement.is, FinalPageElement);
diff --git a/ash/webui/common/resources/cellular_setup/profile_discovery_consent_page.js b/ash/webui/common/resources/cellular_setup/profile_discovery_consent_page.js
index 4826192..c181b8f 100644
--- a/ash/webui/common/resources/cellular_setup/profile_discovery_consent_page.js
+++ b/ash/webui/common/resources/cellular_setup/profile_discovery_consent_page.js
@@ -14,23 +14,39 @@
 import '//resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
 import './base_page.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './profile_discovery_consent_page.html.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'profile-discovery-consent-page',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const ProfileDiscoveryConsentPageElementBase =
+    mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
+/** @polymer */
+class ProfileDiscoveryConsentPageElement extends
+    ProfileDiscoveryConsentPageElementBase {
+  static get is() {
+    return 'profile-discovery-consent-page';
+  }
 
-  properties: {
-    shouldSkipDiscovery: {
-      type: Boolean,
-      notify: true,
-    },
-  },
+  static get template() {
+    return getTemplate();
+  }
+
+  static get properties() {
+    return {
+      shouldSkipDiscovery: {
+        type: Boolean,
+        notify: true,
+      },
+
+    };
+  }
 
   shouldSkipDiscoveryClicked_(e) {
     // A place holder href with the value "#" is used to have a compliant link.
@@ -38,6 +54,12 @@
     e.detail.event.preventDefault();
     e.stopPropagation();
     this.shouldSkipDiscovery = true;
-    this.fire('forward-navigation-requested');
-  },
-});
+    this.dispatchEvent(new CustomEvent('forward-navigation-requested', {
+      bubbles: true,
+      composed: true,
+    }));
+  }
+}
+
+customElements.define(
+    ProfileDiscoveryConsentPageElement.is, ProfileDiscoveryConsentPageElement);
diff --git a/ash/webui/common/resources/cellular_setup/profile_discovery_list_item.js b/ash/webui/common/resources/cellular_setup/profile_discovery_list_item.js
index 52bf1795..faab397 100644
--- a/ash/webui/common/resources/cellular_setup/profile_discovery_list_item.js
+++ b/ash/webui/common/resources/cellular_setup/profile_discovery_list_item.js
@@ -15,43 +15,59 @@
 import '//resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
 import './cellular_setup_icons.html.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {mojoString16ToString} from 'chrome://resources/js/mojo_type_util.js';
 import {ESimProfileProperties} from 'chrome://resources/mojo/chromeos/ash/services/cellular_setup/public/mojom/esim_manager.mojom-webui.js';
 
 import {getTemplate} from './profile_discovery_list_item.html.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'profile-discovery-list-item',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const ProfileDiscoveryListItemElementBase =
+    mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
+/** @polymer */
+class ProfileDiscoveryListItemElement extends
+    ProfileDiscoveryListItemElementBase {
+  static get is() {
+    return 'profile-discovery-list-item';
+  }
 
-  properties: {
-    /**
-     * @type {?ESimProfileProperties}
-     */
-    profileProperties: {
-      type: Object,
-      value: null,
-      notify: true,
-    },
+  static get template() {
+    return getTemplate();
+  }
 
-    selected: {
-      type: Boolean,
-      reflectToAttribute: true,
-    },
+  static get properties() {
+    return {
+      /**
+       * @type {?ESimProfileProperties}
+       */
+      profileProperties: {
+        type: Object,
+        value: null,
+        notify: true,
+      },
 
-    /**
-     * @type {boolean}
-     * @private
-     */
-    isDarkModeActive_: {
-      type: Boolean,
-      value: false,
-    },
-  },
+      selected: {
+        type: Boolean,
+        reflectToAttribute: true,
+      },
+
+      /**
+       * @type {boolean}
+       * @private
+       */
+      isDarkModeActive_: {
+        type: Boolean,
+        value: false,
+      },
+
+    };
+  }
 
   /** @private */
   getProfileName_() {
@@ -59,5 +75,8 @@
       return '';
     }
     return mojoString16ToString(this.profileProperties.name);
-  },
-});
+  }
+}
+
+customElements.define(
+    ProfileDiscoveryListItemElement.is, ProfileDiscoveryListItemElement);
diff --git a/ash/webui/common/resources/cellular_setup/profile_discovery_list_item_legacy.js b/ash/webui/common/resources/cellular_setup/profile_discovery_list_item_legacy.js
index 6ebb7528..1cdb38b 100644
--- a/ash/webui/common/resources/cellular_setup/profile_discovery_list_item_legacy.js
+++ b/ash/webui/common/resources/cellular_setup/profile_discovery_list_item_legacy.js
@@ -15,55 +15,69 @@
 import '//resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
 import './cellular_setup_icons.html.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {mojoString16ToString} from 'chrome://resources/js/mojo_type_util.js';
 import {ESimProfileProperties, ESimProfileRemote} from 'chrome://resources/mojo/chromeos/ash/services/cellular_setup/public/mojom/esim_manager.mojom-webui.js';
 
 import {getTemplate} from './profile_discovery_list_item_legacy.html.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'profile-discovery-list-item-legacy',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const ProfileDiscoveryListItemLegacyElementBase =
+    mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
+/** @polymer */
+class ProfileDiscoveryListItemLegacyElement extends
+    ProfileDiscoveryListItemLegacyElementBase {
+  static get is() {
+    return 'profile-discovery-list-item-legacy';
+  }
 
-  properties: {
-    /** @type {?ESimProfileRemote} */
-    profile: {
-      type: Object,
-      value: null,
-      observer: 'onProfileChanged_',
-    },
+  static get template() {
+    return getTemplate();
+  }
 
-    selected: {
-      type: Boolean,
-      reflectToAttribute: true,
-    },
+  static get properties() {
+    return {
+      /** @type {?ESimProfileRemote} */
+      profile: {
+        type: Object,
+        value: null,
+        observer: 'onProfileChanged_',
+      },
 
-    showLoadingIndicator: {
-      type: Boolean,
-    },
+      selected: {
+        type: Boolean,
+        reflectToAttribute: true,
+      },
 
-    /**
-     * @type {?ESimProfileProperties}
-     * @private
-     */
-    profileProperties_: {
-      type: Object,
-      value: null,
-      notify: true,
-    },
+      showLoadingIndicator: Boolean,
 
-    /**
-     * @type {boolean}
-     * @private
-     */
-    isDarkModeActive_: {
-      type: Boolean,
-      value: false,
-    },
-  },
+      /**
+       * @type {?ESimProfileProperties}
+       * @private
+       */
+      profileProperties_: {
+        type: Object,
+        value: null,
+        notify: true,
+      },
+
+      /**
+       * @type {boolean}
+       * @private
+       */
+      isDarkModeActive_: {
+        type: Boolean,
+        value: false,
+      },
+
+    };
+  }
 
   /** @private */
   onProfileChanged_() {
@@ -74,7 +88,7 @@
     this.profile.getProperties().then(response => {
       this.profileProperties_ = response.properties;
     });
-  },
+  }
 
   /** @private */
   getProfileName_() {
@@ -82,5 +96,9 @@
       return '';
     }
     return mojoString16ToString(this.profileProperties_.name);
-  },
-});
+  }
+}
+
+customElements.define(
+    ProfileDiscoveryListItemLegacyElement.is,
+    ProfileDiscoveryListItemLegacyElement);
diff --git a/ash/webui/common/resources/cellular_setup/profile_discovery_list_page.js b/ash/webui/common/resources/cellular_setup/profile_discovery_list_page.js
index 047ea241..41f99f1 100644
--- a/ash/webui/common/resources/cellular_setup/profile_discovery_list_page.js
+++ b/ash/webui/common/resources/cellular_setup/profile_discovery_list_page.js
@@ -13,36 +13,50 @@
 import './base_page.js';
 import './profile_discovery_list_item.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {ESimProfileProperties} from 'chrome://resources/mojo/chromeos/ash/services/cellular_setup/public/mojom/esim_manager.mojom-webui.js';
 
 import {getTemplate} from './profile_discovery_list_page.html.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'profile-discovery-list-page',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const ProfileDiscoveryListPageElementBase =
+    mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
+/** @polymer */
+class ProfileDiscoveryListPageElement extends
+    ProfileDiscoveryListPageElementBase {
+  static get is() {
+    return 'profile-discovery-list-page';
+  }
 
-  properties: {
-    /**
-     * @type {Array<!ESimProfileProperties>}
-     * @private
-     */
-    pendingProfileProperties: {
-      type: Array,
-    },
+  static get template() {
+    return getTemplate();
+  }
 
-    /**
-     * @type {?ESimProfileProperties}
-     * @private
-     */
-    selectedProfileProperties: {
-      type: Object,
-      notify: true,
-    },
-  },
+  static get properties() {
+    return {
+      /**
+       * @type {Array<!ESimProfileProperties>}
+       * @private
+       */
+      pendingProfileProperties: Array,
+
+      /**
+       * @type {?ESimProfileProperties}
+       * @private
+       */
+      selectedProfileProperties: {
+        type: Object,
+        notify: true,
+      },
+
+    };
+  }
 
   /**
    * @param {ESimProfileProperties} profileProperties
@@ -50,7 +64,7 @@
    */
   isProfilePropertiesSelected_(profileProperties) {
     return this.selectedProfileProperties === profileProperties;
-  },
+  }
 
   /**
    * @param {Event} e
@@ -60,6 +74,12 @@
     e.detail.event.preventDefault();
     e.stopPropagation();
     this.selectedProfileProperties = null;
-    this.fire('forward-navigation-requested');
-  },
-});
+    this.dispatchEvent(new CustomEvent('forward-navigation-requested', {
+      bubbles: true,
+      composed: true,
+    }));
+  }
+}
+
+customElements.define(
+    ProfileDiscoveryListPageElement.is, ProfileDiscoveryListPageElement);
diff --git a/ash/webui/common/resources/cellular_setup/profile_discovery_list_page_legacy.js b/ash/webui/common/resources/cellular_setup/profile_discovery_list_page_legacy.js
index a575939e..d52ead21 100644
--- a/ash/webui/common/resources/cellular_setup/profile_discovery_list_page_legacy.js
+++ b/ash/webui/common/resources/cellular_setup/profile_discovery_list_page_legacy.js
@@ -12,44 +12,59 @@
 import './base_page.js';
 import './profile_discovery_list_item_legacy.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {ESimProfileRemote} from 'chrome://resources/mojo/chromeos/ash/services/cellular_setup/public/mojom/esim_manager.mojom-webui.js';
 
 import {getTemplate} from './profile_discovery_list_page_legacy.html.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'profile-discovery-list-page-legacy',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const ProfileDiscoveryListPageLegacyElementBase =
+    mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
+/** @polymer */
+class ProfileDiscoveryListPageLegacyElement extends
+    ProfileDiscoveryListPageLegacyElementBase {
+  static get is() {
+    return 'profile-discovery-list-page-legacy';
+  }
 
-  properties: {
-    /**
-     * @type {Array<!ESimProfileRemote>}
-     * @private
-     */
-    pendingProfiles: {
-      type: Array,
-    },
+  static get template() {
+    return getTemplate();
+  }
 
-    /**
-     * @type {?ESimProfileRemote}
-     * @private
-     */
-    selectedProfile: {
-      type: Object,
-      notify: true,
-    },
+  static get properties() {
+    return {
+      /**
+       * @type {Array<!ESimProfileRemote>}
+       * @private
+       */
+      pendingProfiles: Array,
 
-    /**
-     * Indicates the UI is busy with an operation and cannot be interacted with.
-     */
-    showBusy: {
-      type: Boolean,
-      value: false,
-    },
-  },
+      /**
+       * @type {?ESimProfileRemote}
+       * @private
+       */
+      selectedProfile: {
+        type: Object,
+        notify: true,
+      },
+
+      /**
+       * Indicates the UI is busy with an operation and cannot be interacted
+       * with.
+       */
+      showBusy: {
+        type: Boolean,
+        value: false,
+      },
+
+    };
+  }
 
   /**
    * @param {ESimProfileRemote} profile
@@ -57,5 +72,9 @@
    */
   isProfileSelected_(profile) {
     return this.selectedProfile === profile;
-  },
-});
+  }
+}
+
+customElements.define(
+    ProfileDiscoveryListPageLegacyElement.is,
+    ProfileDiscoveryListPageLegacyElement);
diff --git a/ash/webui/common/resources/cellular_setup/provisioning_page.js b/ash/webui/common/resources/cellular_setup/provisioning_page.js
index aa734518..ede18e9d 100644
--- a/ash/webui/common/resources/cellular_setup/provisioning_page.js
+++ b/ash/webui/common/resources/cellular_setup/provisioning_page.js
@@ -13,64 +13,79 @@
 import '//resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
 import '//resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
 import {assert} from '//resources/ash/common/assert.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {CellularMetadata} from 'chrome://resources/mojo/chromeos/ash/services/cellular_setup/public/mojom/cellular_setup.mojom-webui.js';
 
 import {CellularSetupDelegate} from './cellular_setup_delegate.js';
 import {getTemplate} from './provisioning_page.html.js';
 import {postDeviceDataToWebview} from './webview_post_util.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'provisioning-page',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const ProvisioningPageElementBase =
+    mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
+/** @polymer */
+export class ProvisioningPageElement extends ProvisioningPageElementBase {
+  static get is() {
+    return 'provisioning-page';
+  }
 
-  properties: {
-    /** @type {!CellularSetupDelegate} */
-    delegate: Object,
+  static get template() {
+    return getTemplate();
+  }
 
-    /**
-     * Whether error state should be shown.
-     * @type {boolean}
-     */
-    showError: {
-      type: Boolean,
-      value: false,
-      notify: true,
-    },
+  static get properties() {
+    return {
+      /** @type {!CellularSetupDelegate} */
+      delegate: Object,
 
-    /**
-     * Metadata used to open carrier provisioning portal. Expected to start as
-     * null, then change to a valid object.
-     * @type {?CellularMetadata}
-     */
-    cellularMetadata: {
-      type: Object,
-      value: null,
-      observer: 'onCellularMetadataChanged_',
-    },
+      /**
+       * Whether error state should be shown.
+       * @type {boolean}
+       */
+      showError: {
+        type: Boolean,
+        value: false,
+        notify: true,
+      },
 
-    /**
-     * Whether the carrier portal has completed being loaded.
-     * @private {boolean}
-     */
-    hasCarrierPortalLoaded_: {
-      type: Boolean,
-      value: false,
-    },
+      /**
+       * Metadata used to open carrier provisioning portal. Expected to start as
+       * null, then change to a valid object.
+       * @type {?CellularMetadata}
+       */
+      cellularMetadata: {
+        type: Object,
+        value: null,
+        observer: 'onCellularMetadataChanged_',
+      },
 
-    /**
-     * The last carrier name provided via |cellularMetadata|.
-     * @private {string}
-     */
-    carrierName_: {
-      type: String,
-      value: '',
-    },
-  },
+      /**
+       * Whether the carrier portal has completed being loaded.
+       * @private {boolean}
+       */
+      hasCarrierPortalLoaded_: {
+        type: Boolean,
+        value: false,
+      },
+
+      /**
+       * The last carrier name provided via |cellularMetadata|.
+       * @private {string}
+       */
+      carrierName_: {
+        type: String,
+        value: '',
+      },
+
+    };
+  }
 
   /**
    * @return {?string}
@@ -87,7 +102,7 @@
       return this.i18n('provisioningPageActiveTitle');
     }
     return this.i18n('provisioningPageLoadingTitle', this.carrierName_);
-  },
+  }
 
   /**
    * @return {?string}
@@ -98,7 +113,7 @@
       return this.i18n('provisioningPageErrorMessage', this.carrierName_);
     }
     return null;
-  },
+  }
 
   /**
    * @return {boolean}
@@ -106,7 +121,7 @@
    */
   shouldShowSpinner_() {
     return !this.showError && !this.hasCarrierPortalLoaded_;
-  },
+  }
 
   /**
    * @return {boolean}
@@ -114,15 +129,15 @@
    */
   shouldShowPortal_() {
     return !this.showError && this.hasCarrierPortalLoaded_;
-  },
+  }
 
   /**
    * @return {?WebView}
    * @private
    */
   getPortalWebview() {
-    return /** @type {?WebView} */ (this.$$('webview'));
-  },
+    return /** @type {?WebView} */ (this.shadowRoot.querySelector('webview'));
+  }
 
   /** @private */
   onCellularMetadataChanged_() {
@@ -136,7 +151,7 @@
     // If |cellularMetadata| is now null, the page should be reset so that a new
     // attempt can begin.
     this.resetPage_();
-  },
+  }
 
   /** @private */
   loadPortal_() {
@@ -165,7 +180,7 @@
 
     // Otherwise, use a normal GET request by specifying the "src".
     portalWebview.src = this.cellularMetadata.paymentUrl.url;
-  },
+  }
 
   /** @private */
   resetPage_() {
@@ -176,12 +191,12 @@
     if (portalWebview) {
       portalWebview.remove();
     }
-  },
+  }
 
   /** @private */
   onPortalLoadAbort_(event) {
     this.showError = true;
-  },
+  }
 
   /** @private */
   onPortalLoadStop_() {
@@ -190,13 +205,14 @@
     }
 
     this.hasCarrierPortalLoaded_ = true;
-    this.fire('carrier-portal-loaded');
+    this.dispatchEvent(new CustomEvent(
+        'carrier-portal-loaded', {bubbles: true, composed: true}));
 
     // When the portal loads, it expects to receive a message from this frame
     // alerting it that loading has completed successfully.
     this.getPortalWebview().contentWindow.postMessage(
         {msg: 'loadedInWebview'}, this.cellularMetadata.paymentUrl.url);
-  },
+  }
 
   /**
    * @param {!Event} event
@@ -223,8 +239,12 @@
     // The <webview> provided an update on the status of the activation attempt.
     if (messageType === 'reportTransactionStatusMsg') {
       const success = status === 'ok';
-      this.fire('on-carrier-portal-result', success);
+      this.dispatchEvent(new CustomEvent(
+          'on-carrier-portal-result',
+          {bubbles: true, composed: true, detail: success}));
       return;
     }
-  },
-});
+  }
+}
+
+customElements.define(ProvisioningPageElement.is, ProvisioningPageElement);
diff --git a/ash/webui/common/resources/cellular_setup/psim_flow_ui.js b/ash/webui/common/resources/cellular_setup/psim_flow_ui.js
index 0c23cb5..7f794fb 100644
--- a/ash/webui/common/resources/cellular_setup/psim_flow_ui.js
+++ b/ash/webui/common/resources/cellular_setup/psim_flow_ui.js
@@ -7,15 +7,18 @@
 import './final_page.js';
 import '//resources/polymer/v3_0/iron-pages/iron-pages.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
 import {assert, assertNotReached} from '//resources/ash/common/assert.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {ActivationDelegateInterface, ActivationDelegateReceiver, ActivationResult, CarrierPortalHandlerRemote, CarrierPortalStatus, CellularMetadata, CellularSetup_StartActivation_ResponseParams, CellularSetupRemote} from 'chrome://resources/mojo/chromeos/ash/services/cellular_setup/public/mojom/cellular_setup.mojom-webui.js';
 
 import {CellularSetupDelegate} from './cellular_setup_delegate.js';
 import {ButtonState} from './cellular_types.js';
+import {FinalPageElement} from './final_page.js';
 import {getCellularSetupRemote} from './mojo_interface_provider.js';
+import {ProvisioningPageElement} from './provisioning_page.js';
 import {getTemplate} from './psim_flow_ui.html.js';
+import {SetupLoadingPageElement} from './setup_loading_page.js';
 import {SubflowBehavior} from './subflow_behavior.js';
 
 /** @enum {string} */
@@ -106,152 +109,161 @@
  * contains navigation buttons and sub-pages corresponding to each step of the
  * flow.
  */
-Polymer({
-  _template: getTemplate(),
-  is: 'psim-flow-ui',
 
-  behaviors: [
-    I18nBehavior,
-    SubflowBehavior,
-  ],
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const PsimFlowUiElementBase =
+    mixinBehaviors([I18nBehavior, SubflowBehavior], PolymerElement);
 
-  properties: {
-    /** @type {!CellularSetupDelegate} */
-    delegate: Object,
+/** @polymer */
+export class PsimFlowUiElement extends PsimFlowUiElementBase {
+  static get is() {
+    return 'psim-flow-ui';
+  }
+
+  static get template() {
+    return getTemplate();
+  }
+
+  static get properties() {
+    return {
+      /** @type {!CellularSetupDelegate} */
+      delegate: Object,
+
+      /**
+       * Carrier name; used in dialog title to show the current carrier
+       * name being setup
+       * @type {string}
+       */
+      nameOfCarrierPendingSetup: {
+        type: String,
+        notify: true,
+        computed: 'getCarrierText(' +
+            'selectedPSimPageName_, cellularMetadata_.*)',
+      },
+
+      forwardButtonLabel: {
+        type: String,
+        notify: true,
+      },
+
+      /**
+       * @type {!PSimUIState}
+       * @private
+       */
+      state_: {
+        type: String,
+        value: PSimUIState.IDLE,
+        observer: 'handlePSimUIStateChange_',
+      },
+
+      /**
+       * Element name of the current selected sub-page.
+       * @type {!PSimPageName}
+       * @private
+       */
+      selectedPSimPageName_: {
+        type: String,
+        value: PSimPageName.SIM_DETECT,
+        notify: true,
+      },
+
+      /**
+       * DOM Element for the current selected sub-page.
+       * @private {!SetupLoadingPageElement|!ProvisioningPageElement|
+       *           !FinalPageElement}
+       */
+      selectedPage_: Object,
+
+      /**
+       * Whether error state should be shown for the current page.
+       * @private {boolean}
+       */
+      showError_: {type: Boolean, value: false},
+
+      /**
+       * Cellular metadata received via the onActivationStarted() callback. If
+       * that callback has not occurred, this field is null.
+       * @private {?CellularMetadata}
+       */
+      cellularMetadata_: {
+        type: Object,
+        value: null,
+      },
+
+      /**
+       * The current number of tries to detect the SIM.
+       * @private {number}
+       */
+      startActivationAttempts_: {
+        type: Number,
+        value: 0,
+      },
+
+    };
+  }
+
+  /** @override */
+  constructor() {
+    super();
 
     /**
-     * Carrier name; used in dialog title to show the current carrier
-     * name being setup
-     * @type {string}
+     * Provides an interface to the CellularSetup Mojo service.
+     * @private {?CellularSetupRemote}
      */
-    nameOfCarrierPendingSetup: {
-      type: String,
-      notify: true,
-      computed: 'getCarrierText(' +
-          'selectedPSimPageName_, cellularMetadata_.*)',
-    },
-
-    forwardButtonLabel: {
-      type: String,
-      notify: true,
-    },
+    this.cellularSetupRemote_ = getCellularSetupRemote();
 
     /**
-     * @type {!PSimUIState}
-     * @private
+     * Delegate responsible for routing activation started/finished events.
+     * @private {?ActivationDelegateReceiver}
      */
-    state_: {
-      type: String,
-      value: PSimUIState.IDLE,
-    },
+    this.activationDelegateReceiver_ = null;
 
     /**
-     * Element name of the current selected sub-page.
-     * @type {!PSimPageName}
-     * @private
+     * The timeout ID corresponding to a timeout for the current state. If no
+     * timeout is active, this value is null.
+     * @private {?number}
      */
-    selectedPSimPageName_: {
-      type: String,
-      value: PSimPageName.SIM_DETECT,
-      notify: true,
-    },
+    this.currentTimeoutId_ = null;
 
     /**
-     * DOM Element for the current selected sub-page.
-     * @private {!SetupLoadingPageElement|!ProvisioningPageElement|
-     *           !FinalPageElement}
+     * Handler used to communicate state updates back to the CellularSetup
+     * service.
+     * @private {?CarrierPortalHandlerRemote}
      */
-    selectedPage_: Object,
+    this.carrierPortalHandler_ = null;
 
     /**
-     * Whether error state should be shown for the current page.
+     * Whether there was a carrier portal error.
      * @private {boolean}
      */
-    showError_: {type: Boolean, value: false},
+    this.didCarrierPortalResultFail_ = false;
 
     /**
-     * Cellular metadata received via the onActivationStarted() callback. If
-     * that callback has not occurred, this field is null.
-     * @private {?CellularMetadata}
+     * The function used to initiate a timer. Can be overwritten in tests.
+     * @private {function(Function, number)}
      */
-    cellularMetadata_: {
-      type: Object,
-      value: null,
-    },
+    this.setTimeoutFunction_ = setTimeout.bind(window);
+  }
+
+  /** @override */
+  connectedCallback() {
+    super.connectedCallback();
 
     /**
-     * The current number of tries to detect the SIM.
-     * @private {number}
+     * The time at which the PSim flow is attached.
+     * @private {?Date}
      */
-    startActivationAttempts_: {
-      type: Number,
-      value: 0,
-    },
-  },
-
-  observers: [
-    'updateShowError_(state_)',
-    'updateSelectedPage_(state_)',
-    'handlePSimUIStateChange_(state_)',
-    'updateButtonBarState_(state_)',
-  ],
-
-  /**
-   * Provides an interface to the CellularSetup Mojo service.
-   * @private {?CellularSetupRemote}
-   */
-  cellularSetupRemote_: null,
-
-  /**
-   * Delegate responsible for routing activation started/finished events.
-   * @private {?ActivationDelegateReceiver}
-   */
-  activationDelegateReceiver_: null,
-
-  /**
-   * The timeout ID corresponding to a timeout for the current state. If no
-   * timeout is active, this value is null.
-   * @private {?number}
-   */
-  currentTimeoutId_: null,
-
-  /**
-   * Handler used to communicate state updates back to the CellularSetup
-   * service.
-   * @private {?CarrierPortalHandlerRemote}
-   */
-  carrierPortalHandler_: null,
-
-  /**
-   * Whether there was a carrier portal error.
-   * @private {boolean}
-   */
-  didCarrierPortalResultFail_: false,
-
-  /**
-   * The function used to initiate a timer. Can be overwritten in tests.
-   * @private {function(Function, number)}
-   */
-  setTimeoutFunction_: setTimeout.bind(window),
-
-  /**
-   * The time at which the PSim flow is attached.
-   * @private {?Date}
-   */
-  timeOnAttached_: null,
-
-  /** @override */
-  created() {
-    this.cellularSetupRemote_ = getCellularSetupRemote();
-  },
-
-  /** @override */
-  attached() {
     this.timeOnAttached_ = new Date();
-  },
+  }
 
   /** @override */
-  detached() {
+  disconnectedCallback() {
+    super.disconnectedCallback();
+
     let resultCode = null;
     switch (this.state_) {
       case PSimUIState.IDLE:
@@ -303,7 +315,7 @@
 
     chrome.metricsPrivate.recordLongTime(
         FAILED_PSIM_SETUP_DURATION_METRIC_NAME, elapsedTimeMs);
-  },
+  }
 
   /**
    * Overrides ActivationDelegateInterface.
@@ -314,14 +326,15 @@
     this.clearTimer_();
     this.cellularMetadata_ = metadata;
     this.state_ = PSimUIState.WAITING_FOR_PORTAL_TO_LOAD;
-  },
+  }
 
   initSubflow() {
     this.state_ = PSimUIState.STARTING_ACTIVATION;
     this.startActivationAttempts_ = 0;
     this.updateButtonBarState_();
-    this.fire('focus-default-button');
-  },
+    this.dispatchEvent(new CustomEvent(
+        'focus-default-button', {bubbles: true, composed: true}));
+  }
 
   navigateForward() {
     switch (this.state_) {
@@ -335,7 +348,8 @@
       case PSimUIState.TIMEOUT_FINISH_ACTIVATION:
       case PSimUIState.FINAL_TIMEOUT_START_ACTIVATION:
       case PSimUIState.ALREADY_ACTIVATED:
-        this.fire('exit-cellular-setup');
+        this.dispatchEvent(new CustomEvent(
+            'exit-cellular-setup', {bubbles: true, composed: true}));
         break;
       case PSimUIState.TIMEOUT_START_ACTIVATION:
         this.state_ = PSimUIState.STARTING_ACTIVATION;
@@ -344,7 +358,7 @@
         assertNotReached();
         break;
     }
-  },
+  }
 
   /**
    * Sets the function used to initiate a timer.
@@ -353,7 +367,7 @@
    */
   setTimerFunctionForTest(timerFunction) {
     this.setTimeoutFunction_ = timerFunction;
-  },
+  }
 
   /** @private */
   updateButtonBarState_() {
@@ -411,7 +425,7 @@
         assertNotReached();
     }
     this.set('buttonState', buttonState);
-  },
+  }
 
   /**
    * Overrides ActivationDelegateInterface.
@@ -434,7 +448,7 @@
       default:
         assertNotReached();
     }
-  },
+  }
 
   /** @private */
   getCarrierText() {
@@ -443,7 +457,7 @@
       return this.cellularMetadata_.carrier;
     }
     return '';
-  },
+  }
 
   /** @private */
   updateShowError_() {
@@ -457,7 +471,7 @@
         this.showError_ = false;
         return;
     }
-  },
+  }
 
   /** @private */
   updateSelectedPage_() {
@@ -484,10 +498,13 @@
       default:
         assertNotReached();
     }
-  },
+  }
 
   /** @private */
   handlePSimUIStateChange_() {
+    this.updateShowError_();
+    this.updateSelectedPage_();
+
     // Since the state has changed, the previous state did not time out, so
     // clear any active timeout.
     this.clearTimer_();
@@ -501,9 +518,10 @@
 
     if (this.state_ === PSimUIState.STARTING_ACTIVATION) {
       this.startActivation_();
-      return;
     }
-  },
+
+    this.updateButtonBarState_();
+  }
 
   /** @private */
   onTimeout_() {
@@ -529,7 +547,7 @@
         // Only the above states are expected to time out.
         assertNotReached();
     }
-  },
+  }
 
   /** @private */
   startActivation_() {
@@ -550,7 +568,7 @@
             (params) => {
               this.carrierPortalHandler_ = params.observer;
             });
-  },
+  }
 
   /** @private */
   closeActivationConnection_() {
@@ -559,7 +577,7 @@
     this.activationDelegateReceiver_ = null;
     this.carrierPortalHandler_ = null;
     this.cellularMetadata_ = null;
-  },
+  }
 
   /** @private */
   clearTimer_() {
@@ -567,14 +585,14 @@
       clearTimeout(this.currentTimeoutId_);
     }
     this.currentTimeoutId_ = null;
-  },
+  }
 
   /** @private */
   onCarrierPortalLoaded_() {
     this.state_ = PSimUIState.WAITING_FOR_USER_PAYMENT;
     this.carrierPortalHandler_.onCarrierPortalStatusChange(
         CarrierPortalStatus.kPortalLoadedWithoutPaidUser);
-  },
+  }
 
   /**
    * @param {!CustomEvent<boolean>} event
@@ -585,7 +603,7 @@
     this.didCarrierPortalResultFail_ = !success;
     this.state_ = success ? PSimUIState.ACTIVATION_SUCCESS :
                             PSimUIState.ACTIVATION_FAILURE;
-  },
+  }
 
   /** @return {string} */
   getLoadingMessage_() {
@@ -595,13 +613,13 @@
       return this.i18n('simDetectPageFinalErrorMessage');
     }
     return this.i18n('establishNetworkConnectionMessage');
-  },
+  }
 
   /** @return {boolean} */
   isSimDetectError_() {
     return this.state_ === PSimUIState.TIMEOUT_START_ACTIVATION ||
         this.state_ === PSimUIState.FINAL_TIMEOUT_START_ACTIVATION;
-  },
+  }
 
   /** @return {string} */
   getLoadingTitle_() {
@@ -609,5 +627,7 @@
       return this.i18n('simDetectPageErrorTitle');
     }
     return '';
-  },
-});
+  }
+}
+
+customElements.define(PsimFlowUiElement.is, PsimFlowUiElement);
diff --git a/ash/webui/common/resources/cellular_setup/setup_loading_page.js b/ash/webui/common/resources/cellular_setup/setup_loading_page.js
index e6e95163..f39beb80 100644
--- a/ash/webui/common/resources/cellular_setup/setup_loading_page.js
+++ b/ash/webui/common/resources/cellular_setup/setup_loading_page.js
@@ -12,42 +12,57 @@
 import 'chrome://resources/cros_components/lottie_renderer/lottie-renderer.js';
 import '//resources/polymer/v3_0/iron-media-query/iron-media-query.js';
 
-import {I18nBehavior} from '//resources/ash/common/i18n_behavior.js';
-import {Polymer} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {I18nBehavior, I18nBehaviorInterface} from '//resources/ash/common/i18n_behavior.js';
+import {mixinBehaviors, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './setup_loading_page.html.js';
 
-Polymer({
-  _template: getTemplate(),
-  is: 'setup-loading-page',
+/**
+ * @constructor
+ * @extends {PolymerElement}
+ * @implements {I18nBehaviorInterface}
+ */
+const SetupLoadingPageElementBase =
+    mixinBehaviors([I18nBehavior], PolymerElement);
 
-  behaviors: [I18nBehavior],
+/** @polymer */
+export class SetupLoadingPageElement extends SetupLoadingPageElementBase {
+  static get is() {
+    return 'setup-loading-page';
+  }
 
-  properties: {
-    /**
-     * Message displayed with spinner when in LOADING state.
-     */
-    loadingMessage: {
-      type: String,
-      value: '',
-    },
+  static get template() {
+    return getTemplate();
+  }
 
-    /**
-     * Title for page if needed.
-     * @type {?string}
-     */
-    loadingTitle: {
-      type: Object,
-      value: '',
-    },
+  static get properties() {
+    return {
+      /**
+       * Message displayed with spinner when in LOADING state.
+       */
+      loadingMessage: {
+        type: String,
+        value: '',
+      },
 
-    /**
-     * Displays a sim detect error graphic if true.
-     */
-    isSimDetectError: {
-      type: Boolean,
-      value: false,
-    },
+      /**
+       * Title for page if needed.
+       * @type {?string}
+       */
+      loadingTitle: {
+        type: Object,
+        value: '',
+      },
 
-  },
-});
+      /**
+       * Displays a sim detect error graphic if true.
+       */
+      isSimDetectError: {
+        type: Boolean,
+        value: false,
+      },
+    };
+  }
+}
+
+customElements.define(SetupLoadingPageElement.is, SetupLoadingPageElement);
diff --git a/ash/webui/common/resources/cr_elements/BUILD.gn b/ash/webui/common/resources/cr_elements/BUILD.gn
index 7f15643..e5928df 100644
--- a/ash/webui/common/resources/cr_elements/BUILD.gn
+++ b/ash/webui/common/resources/cr_elements/BUILD.gn
@@ -23,6 +23,11 @@
     "cr_toast/cr_toast_manager.ts",
   ]
 
+  non_web_component_files = [
+    "cr_container_shadow_mixin.ts",
+    "i18n_mixin.ts",
+  ]
+
   icons_html_files = [ "icons.html" ]
 
   css_files = [
diff --git a/ash/webui/common/resources/cr_elements/cr_container_shadow_mixin.ts b/ash/webui/common/resources/cr_elements/cr_container_shadow_mixin.ts
new file mode 100644
index 0000000..2345da11
--- /dev/null
+++ b/ash/webui/common/resources/cr_elements/cr_container_shadow_mixin.ts
@@ -0,0 +1,174 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview CrContainerShadowMixin holds logic for showing a drop shadow
+ * near the top of a container element, when the content has scrolled.
+ *
+ * Forked from ui/webui/resources/cr_elements/cr_container_shadow_mixin.ts
+ *
+ * Elements using this mixin are expected to define a #container element,
+ * which is the element being scrolled. If the #container element has a
+ * show-bottom-shadow attribute, a drop shadow will also be shown near the
+ * bottom of the container element, when there is additional content to scroll
+ * to. Examples:
+ *
+ * For both top and bottom shadows:
+ * <div id="container" show-bottom-shadow>...</div>
+ *
+ * For top shadow only:
+ * <div id="container">...</div>
+ *
+ * The mixin will take care of inserting an element with ID
+ * 'cr-container-shadow-top' which holds the drop shadow effect, and,
+ * optionally, an element with ID 'cr-container-shadow-bottom' which holds the
+ * same effect. A 'has-shadow' CSS class is automatically added to/removed from
+ * both elements while scrolling, as necessary. Note that the show-bottom-shadow
+ * attribute is inspected only during attached(), and any changes to it that
+ * occur after that point will not be respected.
+ *
+ * Clients should either use the existing shared styling in
+ * cr_shared_style.css, '#cr-container-shadow-[top/bottom]' and
+ * '#cr-container-shadow-[top/bottom].has-shadow', or define their own styles.
+ */
+
+import {assert} from '//resources/js/assert.js';
+import {dedupingMixin, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+
+export enum CrContainerShadowSide {
+  TOP = 'top',
+  BOTTOM = 'bottom',
+}
+
+type Constructor<T> = new (...args: any[]) => T;
+
+export const CrContainerShadowMixin = dedupingMixin(
+    <T extends Constructor<PolymerElement>>(superClass: T): T&
+    Constructor<CrContainerShadowMixinInterface> => {
+      class CrContainerShadowMixin extends superClass implements
+          CrContainerShadowMixinInterface {
+        private intersectionObserver_: IntersectionObserver|null = null;
+        private dropShadows_: Map<CrContainerShadowSide, HTMLDivElement> =
+            new Map();
+        private intersectionProbes_:
+            Map<CrContainerShadowSide, HTMLDivElement> = new Map();
+        private sides_: CrContainerShadowSide[]|null = null;
+
+        override connectedCallback() {
+          super.connectedCallback();
+
+          const hasBottomShadow =
+              this.getContainer_().hasAttribute('show-bottom-shadow');
+          this.sides_ = hasBottomShadow ?
+              [CrContainerShadowSide.TOP, CrContainerShadowSide.BOTTOM] :
+              [CrContainerShadowSide.TOP];
+          this.sides_!.forEach(side => {
+            // The element holding the drop shadow effect to be shown.
+            const shadow = document.createElement('div');
+            shadow.id = `cr-container-shadow-${side}`;
+            shadow.classList.add('cr-container-shadow');
+            this.dropShadows_.set(side, shadow);
+            this.intersectionProbes_.set(side, document.createElement('div'));
+          });
+
+          this.getContainer_().parentNode!.insertBefore(
+              this.dropShadows_.get(CrContainerShadowSide.TOP)!,
+              this.getContainer_());
+          this.getContainer_().prepend(
+              this.intersectionProbes_.get(CrContainerShadowSide.TOP)!);
+
+          if (hasBottomShadow) {
+            this.getContainer_().parentNode!.insertBefore(
+                this.dropShadows_.get(CrContainerShadowSide.BOTTOM)!,
+                this.getContainer_().nextSibling);
+            this.getContainer_().append(
+                this.intersectionProbes_.get(CrContainerShadowSide.BOTTOM)!);
+          }
+
+          this.enableShadowBehavior(true);
+        }
+
+        override disconnectedCallback() {
+          super.disconnectedCallback();
+
+          this.enableShadowBehavior(false);
+        }
+
+        private getContainer_(): HTMLElement {
+          return this.shadowRoot!.querySelector('#container')!;
+        }
+
+        private getIntersectionObserver_(): IntersectionObserver {
+          const callback = (entries: IntersectionObserverEntry[]) => {
+            // In some rare cases, there could be more than one entry per
+            // observed element, in which case the last entry's result
+            // stands.
+            for (const entry of entries) {
+              const target = entry.target;
+              this.sides_!.forEach(side => {
+                if (target === this.intersectionProbes_.get(side)) {
+                  this.dropShadows_.get(side)!.classList.toggle(
+                      'has-shadow', entry.intersectionRatio === 0);
+                }
+              });
+            }
+          };
+          return new IntersectionObserver(
+              callback, {root: this.getContainer_(), threshold: 0});
+        }
+
+        /**
+         * @param enable Whether to enable the mixin or disable it.
+         *     This function does nothing if the mixin is already in the
+         *     requested state.
+         */
+        enableShadowBehavior(enable: boolean) {
+          // Behavior is already enabled/disabled. Return early.
+          if (enable === !!this.intersectionObserver_) {
+            return;
+          }
+
+          if (!enable) {
+            this.intersectionObserver_!.disconnect();
+            this.intersectionObserver_ = null;
+            return;
+          }
+
+          this.intersectionObserver_ = this.getIntersectionObserver_();
+
+          // Need to register the observer within a setTimeout() callback,
+          // otherwise the drop shadow flashes once on startup, because of the
+          // DOM modifications earlier in this function causing a relayout.
+          window.setTimeout(() => {
+            if (this.intersectionObserver_) {
+              // In case this is already detached.
+              this.intersectionProbes_.forEach(probe => {
+                this.intersectionObserver_!.observe(probe);
+              });
+            }
+          });
+        }
+
+        /**
+         * Shows the shadows. The shadow mixin must be disabled before
+         * calling this method, otherwise the intersection observer might
+         * show the shadows again.
+         */
+        showDropShadows() {
+          assert(!this.intersectionObserver_);
+          assert(this.sides_);
+          for (const side of this.sides_) {
+            this.dropShadows_.get(side)!.classList.toggle('has-shadow', true);
+          }
+        }
+      }
+
+      return CrContainerShadowMixin;
+    });
+
+export interface CrContainerShadowMixinInterface {
+  enableShadowBehavior(enable: boolean): void;
+
+  showDropShadows(): void;
+}
diff --git a/ash/webui/common/resources/cr_elements/cr_dialog/cr_dialog.ts b/ash/webui/common/resources/cr_elements/cr_dialog/cr_dialog.ts
index e45ec87..22a1aaddb 100644
--- a/ash/webui/common/resources/cr_elements/cr_dialog/cr_dialog.ts
+++ b/ash/webui/common/resources/cr_elements/cr_dialog/cr_dialog.ts
@@ -26,10 +26,10 @@
 import '../cr_hidden_style.css.js';
 import '../cr_shared_vars.css.js';
 
-import {CrContainerShadowMixin} from '//resources/cr_elements/cr_container_shadow_mixin.js';
 import {assert} from '//resources/js/assert.js';
 import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
+import {CrContainerShadowMixin} from '../cr_container_shadow_mixin.js';
 import {CrIconButtonElement} from '../cr_icon_button/cr_icon_button.js';
 import {CrInputElement} from '../cr_input/cr_input.js';
 
diff --git a/ash/webui/common/resources/cr_elements/i18n_mixin.ts b/ash/webui/common/resources/cr_elements/i18n_mixin.ts
new file mode 100644
index 0000000..ef93dca
--- /dev/null
+++ b/ash/webui/common/resources/cr_elements/i18n_mixin.ts
@@ -0,0 +1,120 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview
+ * 'I18nMixin' is a Mixin offering loading of internationalization
+ * strings. Typically it is used as [[i18n('someString')]] computed bindings or
+ * for this.i18n('foo'). It is not needed for HTML $i18n{otherString}, which is
+ * handled by a C++ templatizer.
+ *
+ * Forked from ui/webui/resources/cr_elements/i18n_mixin.ts
+ */
+
+import {loadTimeData} from '//resources/js/load_time_data.js';
+import {parseHtmlSubset, sanitizeInnerHtml, SanitizeInnerHtmlOpts} from '//resources/js/parse_html_subset.js';
+import {dedupingMixin, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+
+type Constructor<T> = new (...args: any[]) => T;
+
+export const I18nMixin = dedupingMixin(
+    <T extends Constructor<PolymerElement>>(superClass: T): T&
+    Constructor<I18nMixinInterface> => {
+      class I18nMixin extends superClass implements I18nMixinInterface {
+        /**
+         * Returns a translated string where $1 to $9 are replaced by the given
+         * values.
+         * @param id The ID of the string to translate.
+         * @param varArgs Values to replace the placeholders $1 to $9 in the
+         *     string.
+         * @return A translated, substituted string.
+         */
+        private i18nRaw_(id: string, ...varArgs: Array<string|number>) {
+          return varArgs.length === 0 ? loadTimeData.getString(id) :
+                                        loadTimeData.getStringF(id, ...varArgs);
+        }
+
+        /**
+         * Returns a translated string where $1 to $9 are replaced by the given
+         * values. Also sanitizes the output to filter out dangerous HTML/JS.
+         * Use with Polymer bindings that are *not* inner-h-t-m-l.
+         * NOTE: This is not related to $i18n{foo} in HTML, see file overview.
+         * @param id The ID of the string to translate.
+         * @param varArgs Values to replace the placeholders $1 to $9 in the
+         *     string.
+         * @return A translated, sanitized, substituted string.
+         */
+        i18n(id: string, ...varArgs: Array<string|number>) {
+          const rawString = this.i18nRaw_(id, ...varArgs);
+          return parseHtmlSubset(`<b>${rawString}</b>`).firstChild!.textContent!
+              ;
+        }
+
+        /**
+         * Similar to 'i18n', returns a translated, sanitized, substituted
+         * string. It receives the string ID and a dictionary containing the
+         * substitutions as well as optional additional allowed tags and
+         * attributes. Use with Polymer bindings that are inner-h-t-m-l, for
+         * example.
+         * @param id The ID of the string to translate.
+         */
+        i18nAdvanced(id: string, opts?: SanitizeInnerHtmlOpts) {
+          opts = opts || {};
+          const rawString = this.i18nRaw_(id, ...(opts.substitutions || []));
+          return sanitizeInnerHtml(rawString, opts);
+        }
+
+        /**
+         * Similar to 'i18n', with an unused |locale| parameter used to trigger
+         * updates when the locale changes.
+         * @param locale The UI language used.
+         * @param id The ID of the string to translate.
+         * @param varArgs Values to replace the placeholders $1 to $9 in the
+         *     string.
+         * @return A translated, sanitized, substituted string.
+         */
+        i18nDynamic(_locale: string, id: string, ...varArgs: string[]) {
+          return this.i18n(id, ...varArgs);
+        }
+
+        /**
+         * Similar to 'i18nDynamic', but varArgs valus are interpreted as keys
+         * in loadTimeData. This allows generation of strings that take other
+         * localized strings as parameters.
+         * @param locale The UI language used.
+         * @param id The ID of the string to translate.
+         * @param varArgs Values to replace the placeholders $1 to $9
+         *     in the string. Values are interpreted as strings IDs if found in
+         * the list of localized strings.
+         * @return A translated, sanitized, substituted string.
+         */
+        i18nRecursive(locale: string, id: string, ...varArgs: string[]) {
+          let args = varArgs;
+          if (args.length > 0) {
+            // Try to replace IDs with localized values.
+            args = args.map(str => {
+              return this.i18nExists(str) ? loadTimeData.getString(str) : str;
+            });
+          }
+          return this.i18nDynamic(locale, id, ...args);
+        }
+
+        /**
+         * Returns true if a translation exists for |id|.
+         */
+        i18nExists(id: string) {
+          return loadTimeData.valueExists(id);
+        }
+      }
+
+      return I18nMixin;
+    });
+
+export interface I18nMixinInterface {
+  i18n(id: string, ...varArgs: Array<string|number>): string;
+  i18nAdvanced(id: string, opts?: SanitizeInnerHtmlOpts): TrustedHTML;
+  i18nDynamic(locale: string, id: string, ...varArgs: string[]): string;
+  i18nRecursive(locale: string, id: string, ...varArgs: string[]): string;
+  i18nExists(id: string): boolean;
+}
diff --git a/ash/webui/common/resources/network/apn_list.d.ts b/ash/webui/common/resources/network/apn_list.d.ts
index 93b4d72..e3fb920 100644
--- a/ash/webui/common/resources/network/apn_list.d.ts
+++ b/ash/webui/common/resources/network/apn_list.d.ts
@@ -4,6 +4,7 @@
 
 import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {ManagedCellularProperties} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
+import {PortalState} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
 
 export class ApnList extends PolymerElement {
   static get is(): string;
@@ -24,6 +25,9 @@
       value: boolean,
     },
   };
+  errorState: string;
+  portalState: PortalState;
+  shouldOmitLinks: boolean;
   openApnDetailDialogInCreateMode(): void;
   private getApns_;
   private isConnectedApnAutoDetected_: boolean;
diff --git a/ash/webui/common/resources/network/network_property_list_mojo.d.ts b/ash/webui/common/resources/network/network_property_list_mojo.d.ts
index 0756f69..66265d0 100644
--- a/ash/webui/common/resources/network/network_property_list_mojo.d.ts
+++ b/ash/webui/common/resources/network/network_property_list_mojo.d.ts
@@ -6,6 +6,7 @@
 
 interface NetworkPropertyListMojoElement extends LegacyElementMixin,
                                                  HTMLElement {
+  disabled: boolean;
   fields: string[];
 }
 
diff --git a/ash/webui/common/resources/network/network_siminfo.d.ts b/ash/webui/common/resources/network/network_siminfo.d.ts
index 358c7d1b..bb0033c0 100644
--- a/ash/webui/common/resources/network/network_siminfo.d.ts
+++ b/ash/webui/common/resources/network/network_siminfo.d.ts
@@ -7,6 +7,7 @@
 import {LegacyElementMixin} from 'chrome://resources/polymer/v3_0/polymer/lib/legacy/legacy-element-mixin.js';
 
 interface NetworkSiminfoElement extends LegacyElementMixin, HTMLElement {
+  disabled: boolean;
   getUnlockButton(): CrButtonElement|null;
   getSimLockToggle(): CrToggleElement|null;
 }
diff --git a/ash/webui/scanning/resources/BUILD.gn b/ash/webui/scanning/resources/BUILD.gn
index 5d1ce0e..be2246c 100644
--- a/ash/webui/scanning/resources/BUILD.gn
+++ b/ash/webui/scanning/resources/BUILD.gn
@@ -80,7 +80,6 @@
     "//ash/webui/common/resources/cr_elements:build_ts",
     "//third_party/polymer/v3_0:library",
     "//ui/webui/resources/cr_components/color_change_listener:build_ts",
-    "//ui/webui/resources/cr_elements:build_ts",
     "//ui/webui/resources/js:build_ts",
     "//ui/webui/resources/mojo:build_ts",
   ]
diff --git a/ash/webui/scanning/resources/action_toolbar.ts b/ash/webui/scanning/resources/action_toolbar.ts
index 57c870d..6e5574d 100644
--- a/ash/webui/scanning/resources/action_toolbar.ts
+++ b/ash/webui/scanning/resources/action_toolbar.ts
@@ -7,7 +7,7 @@
 import 'chrome://resources/ash/common/cr_elements/cr_icon_button/cr_icon_button.js';
 
 import {assert} from 'chrome://resources/ash/common/assert.js';
-import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
+import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './action_toolbar.html.js';
diff --git a/ash/webui/scanning/resources/color_mode_select.ts b/ash/webui/scanning/resources/color_mode_select.ts
index 9cf71c1..4d76c7a 100644
--- a/ash/webui/scanning/resources/color_mode_select.ts
+++ b/ash/webui/scanning/resources/color_mode_select.ts
@@ -6,7 +6,7 @@
 import './strings.m.js';
 
 import {assert} from 'chrome://resources/ash/common/assert.js';
-import {I18nMixin, I18nMixinInterface} from 'chrome://resources/cr_elements/i18n_mixin.js';
+import {I18nMixin, I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './color_mode_select.html.js';
diff --git a/ash/webui/scanning/resources/file_type_select.ts b/ash/webui/scanning/resources/file_type_select.ts
index c96ee66c..72603c5 100644
--- a/ash/webui/scanning/resources/file_type_select.ts
+++ b/ash/webui/scanning/resources/file_type_select.ts
@@ -5,7 +5,7 @@
 import './scan_settings_section.js';
 import './strings.m.js';
 
-import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
+import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './file_type_select.html.js';
diff --git a/ash/webui/scanning/resources/loading_page.ts b/ash/webui/scanning/resources/loading_page.ts
index 2973172..95f08e4 100644
--- a/ash/webui/scanning/resources/loading_page.ts
+++ b/ash/webui/scanning/resources/loading_page.ts
@@ -7,8 +7,8 @@
 import 'chrome://resources/polymer/v3_0/paper-progress/paper-progress.js';
 import './strings.m.js';
 
+import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
-import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './loading_page.html.js';
diff --git a/ash/webui/scanning/resources/multi_page_checkbox.ts b/ash/webui/scanning/resources/multi_page_checkbox.ts
index eadd70a..6d4d5a8 100644
--- a/ash/webui/scanning/resources/multi_page_checkbox.ts
+++ b/ash/webui/scanning/resources/multi_page_checkbox.ts
@@ -6,7 +6,7 @@
 import './scan_settings_section.js';
 import './strings.m.js';
 
-import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
+import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './multi_page_checkbox.html.js';
diff --git a/ash/webui/scanning/resources/multi_page_scan.ts b/ash/webui/scanning/resources/multi_page_scan.ts
index 1cfb854..79468091 100644
--- a/ash/webui/scanning/resources/multi_page_scan.ts
+++ b/ash/webui/scanning/resources/multi_page_scan.ts
@@ -7,7 +7,7 @@
 import './scanning_fonts.css.js';
 import './strings.m.js';
 
-import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
+import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './multi_page_scan.html.js';
diff --git a/ash/webui/scanning/resources/page_size_select.ts b/ash/webui/scanning/resources/page_size_select.ts
index 935e7115..59fce8b 100644
--- a/ash/webui/scanning/resources/page_size_select.ts
+++ b/ash/webui/scanning/resources/page_size_select.ts
@@ -6,7 +6,7 @@
 import './strings.m.js';
 
 import {assert} from 'chrome://resources/ash/common/assert.js';
-import {I18nMixin, I18nMixinInterface} from 'chrome://resources/cr_elements/i18n_mixin.js';
+import {I18nMixin, I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './page_size_select.html.js';
diff --git a/ash/webui/scanning/resources/resolution_select.ts b/ash/webui/scanning/resources/resolution_select.ts
index 6f2fc9ef..3b6f482 100644
--- a/ash/webui/scanning/resources/resolution_select.ts
+++ b/ash/webui/scanning/resources/resolution_select.ts
@@ -6,7 +6,7 @@
 import './strings.m.js';
 
 import {assert} from 'chrome://resources/ash/common/assert.js';
-import {I18nMixin, I18nMixinInterface} from 'chrome://resources/cr_elements/i18n_mixin.js';
+import {I18nMixin, I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './resolution_select.html.js';
diff --git a/ash/webui/scanning/resources/scan_done_section.ts b/ash/webui/scanning/resources/scan_done_section.ts
index 4cf14e7..4e47fd14 100644
--- a/ash/webui/scanning/resources/scan_done_section.ts
+++ b/ash/webui/scanning/resources/scan_done_section.ts
@@ -9,9 +9,9 @@
 import './strings.m.js';
 
 import {assert} from 'chrome://resources/ash/common/assert.js';
+import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
 import {strictQuery} from 'chrome://resources/ash/common/typescript_utils/strict_query.js';
-import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
 import {sanitizeInnerHtml} from 'chrome://resources/js/parse_html_subset.js';
 import {FilePath} from 'chrome://resources/mojo/mojo/public/mojom/base/file_path.mojom-webui.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
diff --git a/ash/webui/scanning/resources/scan_preview.ts b/ash/webui/scanning/resources/scan_preview.ts
index 1b22288..bad596c7 100644
--- a/ash/webui/scanning/resources/scan_preview.ts
+++ b/ash/webui/scanning/resources/scan_preview.ts
@@ -13,8 +13,8 @@
 import {assert} from 'chrome://resources/ash/common/assert.js';
 import {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
 import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
+import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
-import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
 import {DomRepeatEvent, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {ForceHiddenElementsVisibleObserverInterface, ForceHiddenElementsVisibleObserverReceiver} from './accessibility_features.mojom-webui.js';
diff --git a/ash/webui/scanning/resources/scan_to_select.ts b/ash/webui/scanning/resources/scan_to_select.ts
index 2d4efe07..28f23bf5 100644
--- a/ash/webui/scanning/resources/scan_to_select.ts
+++ b/ash/webui/scanning/resources/scan_to_select.ts
@@ -5,8 +5,8 @@
 import './scan_settings_section.js';
 import './strings.m.js';
 
+import {I18nMixin, I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {strictQuery} from 'chrome://resources/ash/common/typescript_utils/strict_query.js';
-import {I18nMixin, I18nMixinInterface} from 'chrome://resources/cr_elements/i18n_mixin.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './scan_to_select.html.js';
diff --git a/ash/webui/scanning/resources/scanner_select.ts b/ash/webui/scanning/resources/scanner_select.ts
index 62bc0353..4dfdb564 100644
--- a/ash/webui/scanning/resources/scanner_select.ts
+++ b/ash/webui/scanning/resources/scanner_select.ts
@@ -8,8 +8,8 @@
 import './scan_settings_section.js';
 import './strings.m.js';
 
+import {I18nMixin, I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {strictQuery} from 'chrome://resources/ash/common/typescript_utils/strict_query.js';
-import {I18nMixin, I18nMixinInterface} from 'chrome://resources/cr_elements/i18n_mixin.js';
 import {afterNextRender, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './scanner_select.html.js';
diff --git a/ash/webui/scanning/resources/scanning_app.ts b/ash/webui/scanning/resources/scanning_app.ts
index 7e5b6e4..62c6b3c 100644
--- a/ash/webui/scanning/resources/scanning_app.ts
+++ b/ash/webui/scanning/resources/scanning_app.ts
@@ -30,13 +30,13 @@
 
 import {assert} from 'chrome://resources/ash/common/assert.js';
 import {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
+import {CrContainerShadowMixin, CrContainerShadowMixinInterface} from 'chrome://resources/ash/common/cr_elements/cr_container_shadow_mixin.js';
 import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
 import {CrToastElement} from 'chrome://resources/ash/common/cr_elements/cr_toast/cr_toast.js';
+import {I18nMixin, I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
 import {strictQuery} from 'chrome://resources/ash/common/typescript_utils/strict_query.js';
 import {ColorChangeUpdater} from 'chrome://resources/cr_components/color_change_listener/colors_css_updater.js';
-import {CrContainerShadowMixin, CrContainerShadowMixinInterface} from 'chrome://resources/cr_elements/cr_container_shadow_mixin.js';
-import {I18nMixin, I18nMixinInterface} from 'chrome://resources/cr_elements/i18n_mixin.js';
 import {FilePath} from 'chrome://resources/mojo/mojo/public/mojom/base/file_path.mojom-webui.js';
 import {UnguessableToken} from 'chrome://resources/mojo/mojo/public/mojom/base/unguessable_token.mojom-webui.js';
 import {IronCollapseElement} from 'chrome://resources/polymer/v3_0/iron-collapse/iron-collapse.js';
diff --git a/ash/webui/scanning/resources/source_select.ts b/ash/webui/scanning/resources/source_select.ts
index 0c68cd9e..9d15bf7 100644
--- a/ash/webui/scanning/resources/source_select.ts
+++ b/ash/webui/scanning/resources/source_select.ts
@@ -6,7 +6,7 @@
 import './strings.m.js';
 
 import {assert} from 'chrome://resources/ash/common/assert.js';
-import {I18nMixin, I18nMixinInterface} from 'chrome://resources/cr_elements/i18n_mixin.js';
+import {I18nMixin, I18nMixinInterface} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {ScanSource, SourceType} from './scanning.mojom-webui.js';
diff --git a/ash/webui/shortcut_customization_ui/backend/BUILD.gn b/ash/webui/shortcut_customization_ui/backend/BUILD.gn
index 0e0f298..98f2b34 100644
--- a/ash/webui/shortcut_customization_ui/backend/BUILD.gn
+++ b/ash/webui/shortcut_customization_ui/backend/BUILD.gn
@@ -20,6 +20,8 @@
     "search/search_concept_registry.h",
     "search/search_handler.cc",
     "search/search_handler.h",
+    "text_accelerator_part.cc",
+    "text_accelerator_part.h",
   ]
 
   deps = [
diff --git a/ash/webui/shortcut_customization_ui/backend/accelerator_layout_table.cc b/ash/webui/shortcut_customization_ui/backend/accelerator_layout_table.cc
index 90103649..b360c5cd 100644
--- a/ash/webui/shortcut_customization_ui/backend/accelerator_layout_table.cc
+++ b/ash/webui/shortcut_customization_ui/backend/accelerator_layout_table.cc
@@ -25,57 +25,6 @@
 
 namespace ash {
 
-namespace {
-
-std::u16string GetTextForModifier(ui::EventFlags modifier) {
-  switch (modifier) {
-    case ui::EF_SHIFT_DOWN:
-      return u"shift";
-    case ui::EF_CONTROL_DOWN:
-      return u"ctrl";
-    case ui::EF_ALT_DOWN:
-      return u"alt";
-    case ui::EF_COMMAND_DOWN:
-      return u"meta";
-  }
-  NOTREACHED();
-  return std::u16string();
-}
-
-std::u16string GetTextForDelimiter(TextAcceleratorDelimiter delimiter) {
-  // Note: Use a switch statement to perform string lookup if/when more
-  // delimiters are added to the TextAcceleratorDelimiter enum.
-  CHECK_EQ(delimiter, TextAcceleratorDelimiter::kPlusSign);
-  return u"+";
-}
-
-}  // namespace
-
-TextAcceleratorPart::TextAcceleratorPart(ui::EventFlags modifier) {
-  text = GetTextForModifier(modifier);
-  type = mojom::TextAcceleratorPartType::kModifier;
-}
-
-TextAcceleratorPart::TextAcceleratorPart(ui::KeyboardCode key_code) {
-  type = mojom::TextAcceleratorPartType::kKey;
-  keycode = key_code;
-}
-
-TextAcceleratorPart::TextAcceleratorPart(const std::u16string& plain_text) {
-  text = plain_text;
-  type = mojom::TextAcceleratorPartType::kPlainText;
-}
-
-TextAcceleratorPart::TextAcceleratorPart(TextAcceleratorDelimiter delimiter) {
-  text = GetTextForDelimiter(delimiter);
-  type = mojom::TextAcceleratorPartType::kDelimiter;
-}
-
-TextAcceleratorPart::TextAcceleratorPart(const TextAcceleratorPart&) = default;
-TextAcceleratorPart::~TextAcceleratorPart() = default;
-TextAcceleratorPart& TextAcceleratorPart::operator=(
-    const TextAcceleratorPart&) = default;
-
 // Constructor used for text-based layout accelerators.
 NonConfigurableAcceleratorDetails::NonConfigurableAcceleratorDetails(
     int message_id,
diff --git a/ash/webui/shortcut_customization_ui/backend/accelerator_layout_table.h b/ash/webui/shortcut_customization_ui/backend/accelerator_layout_table.h
index c314198..f38def8 100644
--- a/ash/webui/shortcut_customization_ui/backend/accelerator_layout_table.h
+++ b/ash/webui/shortcut_customization_ui/backend/accelerator_layout_table.h
@@ -14,6 +14,7 @@
 #include "ash/public/cpp/accelerators.h"
 #include "ash/public/mojom/accelerator_info.mojom.h"
 #include "ash/strings/grit/ash_strings.h"
+#include "ash/webui/shortcut_customization_ui/backend/text_accelerator_part.h"
 #include "base/containers/fixed_flat_set.h"
 #include "ui/base/accelerators/accelerator.h"
 #include "ui/events/event_constants.h"
@@ -157,11 +158,6 @@
   kAmbientMoveToEndOfWord,
 };
 
-// Used to separate text accelerator parts in the UI e.g ctrl + 1.
-enum TextAcceleratorDelimiter {
-  kPlusSign,
-};
-
 // Contains details for UI styling of an accelerator.
 struct AcceleratorLayoutDetails {
   // The accelerator action id associated for a source. Concat `source` and
@@ -189,25 +185,6 @@
   mojom::AcceleratorSource source;
 };
 
-// Represents a replacement for part of a non-configurable accelerator.
-// Contains the text to display as well as its type (Modifier, Key, Plain Text)
-// which is needed to determine how to display the text in the shortcut
-// customization app.
-class TextAcceleratorPart : public mojom::TextAcceleratorPart {
- public:
-  explicit TextAcceleratorPart(ui::EventFlags modifier);
-  explicit TextAcceleratorPart(ui::KeyboardCode key_code);
-  explicit TextAcceleratorPart(const std::u16string& plain_text);
-  explicit TextAcceleratorPart(TextAcceleratorDelimiter delimiter);
-  TextAcceleratorPart(const TextAcceleratorPart&);
-  TextAcceleratorPart& operator=(const TextAcceleratorPart&);
-  ~TextAcceleratorPart();
-
-  // If the part is a keycode, we store it so that we will always have a way
-  // to get the accurate localized key string to display.
-  std::optional<ui::KeyboardCode> keycode;
-};
-
 // Contains info related to a non-configurable accelerator. A non-configurable
 // accelerator can contain either a standard or text-based accelerator. The
 // message_id and list of replacements will be provided when dealing
diff --git a/ash/webui/shortcut_customization_ui/backend/text_accelerator_part.cc b/ash/webui/shortcut_customization_ui/backend/text_accelerator_part.cc
new file mode 100644
index 0000000..0a015bd
--- /dev/null
+++ b/ash/webui/shortcut_customization_ui/backend/text_accelerator_part.cc
@@ -0,0 +1,61 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/webui/shortcut_customization_ui/backend/text_accelerator_part.h"
+
+#include <optional>
+#include <string>
+
+namespace ash {
+
+namespace {
+std::u16string GetTextForModifier(ui::EventFlags modifier) {
+  switch (modifier) {
+    case ui::EF_SHIFT_DOWN:
+      return u"shift";
+    case ui::EF_CONTROL_DOWN:
+      return u"ctrl";
+    case ui::EF_ALT_DOWN:
+      return u"alt";
+    case ui::EF_COMMAND_DOWN:
+      return u"meta";
+  }
+  NOTREACHED();
+  return std::u16string();
+}
+
+std::u16string GetTextForDelimiter(TextAcceleratorDelimiter delimiter) {
+  // Note: Use a switch statement to perform string lookup if/when more
+  // delimiters are added to the TextAcceleratorDelimiter enum.
+  CHECK_EQ(delimiter, TextAcceleratorDelimiter::kPlusSign);
+  return u"+";
+}
+}  // namespace
+
+TextAcceleratorPart::TextAcceleratorPart(ui::EventFlags modifier) {
+  text = GetTextForModifier(modifier);
+  type = mojom::TextAcceleratorPartType::kModifier;
+}
+
+TextAcceleratorPart::TextAcceleratorPart(ui::KeyboardCode key_code) {
+  type = mojom::TextAcceleratorPartType::kKey;
+  keycode = key_code;
+}
+
+TextAcceleratorPart::TextAcceleratorPart(const std::u16string& plain_text) {
+  text = plain_text;
+  type = mojom::TextAcceleratorPartType::kPlainText;
+}
+
+TextAcceleratorPart::TextAcceleratorPart(TextAcceleratorDelimiter delimiter) {
+  text = GetTextForDelimiter(delimiter);
+  type = mojom::TextAcceleratorPartType::kDelimiter;
+}
+
+TextAcceleratorPart::TextAcceleratorPart(const TextAcceleratorPart&) = default;
+TextAcceleratorPart::~TextAcceleratorPart() = default;
+TextAcceleratorPart& TextAcceleratorPart::operator=(
+    const TextAcceleratorPart&) = default;
+
+}  // namespace ash
diff --git a/ash/webui/shortcut_customization_ui/backend/text_accelerator_part.h b/ash/webui/shortcut_customization_ui/backend/text_accelerator_part.h
new file mode 100644
index 0000000..939be29
--- /dev/null
+++ b/ash/webui/shortcut_customization_ui/backend/text_accelerator_part.h
@@ -0,0 +1,43 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef ASH_WEBUI_SHORTCUT_CUSTOMIZATION_UI_BACKEND_TEXT_ACCELERATOR_PART_H_
+#define ASH_WEBUI_SHORTCUT_CUSTOMIZATION_UI_BACKEND_TEXT_ACCELERATOR_PART_H_
+
+#include <cstdint>
+#include <optional>
+#include <string>
+
+#include "ash/public/mojom/accelerator_info.mojom.h"
+#include "ui/events/event_constants.h"
+
+namespace ash {
+
+// Used to separate text accelerator parts in the UI e.g ctrl + 1.
+enum TextAcceleratorDelimiter {
+  kPlusSign,
+};
+
+// Represents a replacement for part of a non-configurable accelerator.
+// Contains the text to display as well as its type (Modifier, Key, Plain Text)
+// which is needed to determine how to display the text in the shortcut
+// customization app.
+class TextAcceleratorPart : public mojom::TextAcceleratorPart {
+ public:
+  explicit TextAcceleratorPart(ui::EventFlags modifier);
+  explicit TextAcceleratorPart(ui::KeyboardCode key_code);
+  explicit TextAcceleratorPart(const std::u16string& plain_text);
+  explicit TextAcceleratorPart(TextAcceleratorDelimiter delimiter);
+  TextAcceleratorPart(const TextAcceleratorPart&);
+  TextAcceleratorPart& operator=(const TextAcceleratorPart&);
+  ~TextAcceleratorPart();
+
+  // If the part is a keycode, we store it so that we will always have a way
+  // to get the accurate localized key string to display.
+  std::optional<ui::KeyboardCode> keycode;
+};
+
+}  // namespace ash
+
+#endif  // ASH_WEBUI_SHORTCUT_CUSTOMIZATION_UI_BACKEND_TEXT_ACCELERATOR_PART_H_
diff --git a/ash/wm/overview/glanceables/glanceables_bar_view.cc b/ash/wm/overview/birch/birch_bar_view.cc
similarity index 76%
rename from ash/wm/overview/glanceables/glanceables_bar_view.cc
rename to ash/wm/overview/birch/birch_bar_view.cc
index a94f7bf6..35b41e8 100644
--- a/ash/wm/overview/glanceables/glanceables_bar_view.cc
+++ b/ash/wm/overview/birch/birch_bar_view.cc
@@ -1,8 +1,8 @@
-// Copyright 2023 The Chromium Authors
+// Copyright 2024 The Chromium Authors
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/wm/overview/glanceables/glanceables_bar_view.h"
+#include "ash/wm/overview/birch/birch_bar_view.h"
 
 #include "ash/public/cpp/shell_window_ids.h"
 #include "ash/resources/vector_icons/vector_icons.h"
@@ -39,15 +39,14 @@
 }  // namespace
 
 //------------------------------------------------------------------------------
-// GlanceablesBarView::GlanceablesChipsContainer
-// The chips container with glanceables chips and hiding chips button.
-class GlanceablesBarView::GlanceablesChipsContainer
-    : public views::BoxLayoutView {
-  METADATA_HEADER(GlanceablesChipsContainer, views::BoxLayoutView)
+// BirchBarView::BirchChipsContainer
+// The chips container with birch chips and hiding chips button.
+class BirchBarView::BirchChipsContainer : public views::BoxLayoutView {
+  METADATA_HEADER(BirchChipsContainer, views::BoxLayoutView)
 
  public:
-  explicit GlanceablesChipsContainer(GlanceablesBarView* glanceable_bar)
-      : glanceable_bar_(glanceable_bar) {
+  explicit BirchChipsContainer(BirchBarView* birch_bar)
+      : birch_bar_(birch_bar) {
     SetPaintToLayer();
     layer()->SetFillsBoundsOpaquely(false);
     SetOrientation(views::BoxLayout::Orientation::kHorizontal);
@@ -55,21 +54,20 @@
     SetCrossAxisAlignment(views::BoxLayout::CrossAxisAlignment::kCenter);
     SetBetweenChildSpacing(kChipSpacing);
     hide_chips_button_ = AddChildView(std::make_unique<IconButton>(
-        base::BindRepeating(&GlanceablesBarView::OnShowHideChipsButtonPressed,
-                            base::Unretained(glanceable_bar_), /*show=*/false),
+        base::BindRepeating(&BirchBarView::OnShowHideChipsButtonPressed,
+                            base::Unretained(birch_bar_), /*show=*/false),
         IconButton::Type::kMedium, &kChevronDownIcon, u"Hide", false, false));
     hide_chips_button_->SetProperty(views::kMarginsKey, kHideButtonMargin);
     hide_chips_button_->SetEnableBlurredBackgroundShield(true);
   }
 
-  GlanceablesChipsContainer(const GlanceablesChipsContainer&) = delete;
-  GlanceablesChipsContainer& operator=(const GlanceablesChipsContainer&) =
-      delete;
-  ~GlanceablesChipsContainer() override = default;
+  BirchChipsContainer(const BirchChipsContainer&) = delete;
+  BirchChipsContainer& operator=(const BirchChipsContainer&) = delete;
+  ~BirchChipsContainer() override = default;
 
-  void AddChip(std::unique_ptr<GlanceablesChipButton> chip) {
+  void AddChip(std::unique_ptr<BirchChipButton> chip) {
     if (static_cast<int>(chips_.size()) == kMaxChipsNum) {
-      NOTREACHED() << "The number of glanceable chips reaches the limit of 4";
+      NOTREACHED() << "The number of birch chips reaches the limit of 4";
       return;
     }
     const size_t child_num = children().size();
@@ -78,7 +76,7 @@
     chips_.push_back(AddChildViewAt(std::move(chip), child_num - 1));
   }
 
-  void RemoveChip(GlanceablesChipButton* chip) {
+  void RemoveChip(BirchChipButton* chip) {
     auto iter = base::ranges::find(chips_, chip);
     if (iter != chips_.end()) {
       RemoveChildViewT(chip);
@@ -88,21 +86,18 @@
   }
 
  private:
-  raw_ptr<GlanceablesBarView> glanceable_bar_;
-  std::vector<raw_ptr<GlanceablesChipButton>> chips_;
+  raw_ptr<BirchBarView> birch_bar_;
+  std::vector<raw_ptr<BirchChipButton>> chips_;
   raw_ptr<IconButton> hide_chips_button_;
 };
 
-BEGIN_METADATA(GlanceablesBarView,
-               GlanceablesChipsContainer,
-               views::BoxLayoutView)
+BEGIN_METADATA(BirchBarView, BirchChipsContainer, views::BoxLayoutView)
 END_METADATA
 
 //------------------------------------------------------------------------------
-// GlanceablesBarView
-GlanceablesBarView::GlanceablesBarView() {
-  chips_container_ =
-      AddChildView(std::make_unique<GlanceablesChipsContainer>(this));
+// BirchBarView
+BirchBarView::BirchBarView() {
+  chips_container_ = AddChildView(std::make_unique<BirchChipsContainer>(this));
 
   show_chips_button_container_ = AddChildView(std::make_unique<views::View>());
   show_chips_button_container_->SetPaintToLayer();
@@ -111,7 +106,7 @@
 
   auto* show_chips_button =
       show_chips_button_container_->AddChildView(std::make_unique<IconButton>(
-          base::BindRepeating(&GlanceablesBarView::OnShowHideChipsButtonPressed,
+          base::BindRepeating(&BirchBarView::OnShowHideChipsButtonPressed,
                               base::Unretained(this), /*show=*/true),
           IconButton::Type::kMedium, &kChevronUpIcon, u"Show", false, false));
   show_chips_button->SetEnableBlurredBackgroundShield(true);
@@ -119,10 +114,10 @@
   chips_container_->SetVisible(false);
 }
 
-GlanceablesBarView::~GlanceablesBarView() = default;
+BirchBarView::~BirchBarView() = default;
 
-void GlanceablesBarView::ShowWidgetForTesting(
-    std::unique_ptr<GlanceablesBarView> bar_view) {
+void BirchBarView::ShowWidgetForTesting(
+    std::unique_ptr<BirchBarView> bar_view) {
   views::Widget::InitParams params;
   params.type = views::Widget::InitParams::TYPE_POPUP;
   params.layer_type = ui::LAYER_NOT_DRAWN;
@@ -145,7 +140,7 @@
   g_widget_for_testing->Show();
 }
 
-void GlanceablesBarView::HideWidgetForTesting() {
+void BirchBarView::HideWidgetForTesting() {
   if (g_widget_for_testing) {
     g_widget_for_testing->CloseWithReason(
         views::Widget::ClosedReason::kUnspecified);
@@ -153,14 +148,14 @@
   }
 }
 
-void GlanceablesBarView::AddChip(
+void BirchBarView::AddChip(
     const ui::ImageModel& icon,
     const std::u16string& title,
     const std::u16string& sub_title,
     views::Button::PressedCallback callback,
     std::optional<std::u16string> button_title,
     std::optional<views::Button::PressedCallback> button_callback) {
-  auto chip = views::Builder<GlanceablesChipButton>()
+  auto chip = views::Builder<BirchChipButton>()
                   .SetIconImage(icon)
                   .SetTitleText(title)
                   .SetSubtitleText(sub_title)
@@ -174,7 +169,7 @@
   chips_container_->AddChip(std::move(chip));
 }
 
-gfx::Size GlanceablesBarView::CalculatePreferredSize() const {
+gfx::Size BirchBarView::CalculatePreferredSize() const {
   gfx::Size preferred_size;
   for (views::View* content : children()) {
     preferred_size.SetToMax(content->GetPreferredSize());
@@ -182,11 +177,11 @@
   return gfx::Size(preferred_size.width(), kBarHeight);
 }
 
-int GlanceablesBarView::GetHeightForWidth(int width) const {
+int BirchBarView::GetHeightForWidth(int width) const {
   return kBarHeight;
 }
 
-void GlanceablesBarView::Layout() {
+void BirchBarView::Layout() {
   // Centralize the chips container/show button.
   const gfx::Point center_point = GetContentsBounds().CenterPoint();
   for (views::View* content : children()) {
@@ -196,11 +191,11 @@
   }
 }
 
-void GlanceablesBarView::RemoveChip(GlanceablesChipButton* chip) {
+void BirchBarView::RemoveChip(BirchChipButton* chip) {
   chips_container_->RemoveChip(chip);
 }
 
-void GlanceablesBarView::OnAnimationsEnded(bool show) {
+void BirchBarView::OnAnimationsEnded(bool show) {
   // Update contents visibility and opacity on animation completed or aborted.
   animation_in_progress_ = false;
   if (show) {
@@ -210,7 +205,7 @@
   }
 }
 
-void GlanceablesBarView::OnShowHideChipsButtonPressed(bool show) {
+void BirchBarView::OnShowHideChipsButtonPressed(bool show) {
   if (animation_in_progress_) {
     return;
   }
@@ -235,7 +230,7 @@
 
   // Setup animations.
   auto animation_complete_callback = base::BindRepeating(
-      &GlanceablesBarView::OnAnimationsEnded, base::Unretained(this), show);
+      &BirchBarView::OnAnimationsEnded, base::Unretained(this), show);
   views::AnimationBuilder animation_builder;
   animation_builder.OnEnded(base::OnceClosure(animation_complete_callback))
       .OnAborted(base::OnceClosure(animation_complete_callback))
@@ -248,7 +243,7 @@
                     show ? gfx::Transform() : vertical_shift);
 }
 
-BEGIN_METADATA(GlanceablesBarView)
+BEGIN_METADATA(BirchBarView)
 END_METADATA
 
 }  // namespace ash
diff --git a/ash/wm/overview/birch/birch_bar_view.h b/ash/wm/overview/birch/birch_bar_view.h
new file mode 100644
index 0000000..ded7da5
--- /dev/null
+++ b/ash/wm/overview/birch/birch_bar_view.h
@@ -0,0 +1,74 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef ASH_WM_OVERVIEW_BIRCH_BIRCH_BAR_VIEW_H_
+#define ASH_WM_OVERVIEW_BIRCH_BIRCH_BAR_VIEW_H_
+
+#include "ash/wm/overview/birch/birch_chip_button.h"
+#include "ui/base/metadata/metadata_header_macros.h"
+#include "ui/base/models/image_model.h"
+#include "ui/views/controls/button/button.h"
+#include "ui/views/view.h"
+
+namespace ash {
+
+// The bar container to show/hide birch chips. The birch chips will
+// be shown in a row with a hiding chips button at the end. When pressing the
+// hiding button, the birch chips will fade out and the showing chips
+// button will appear in the center.
+class BirchBarView : public views::View, public BirchChipButton::Delegate {
+  METADATA_HEADER(BirchBarView, views::View)
+
+ public:
+  // TODO(zxdan): When the data model is implemented, pass in the model to
+  // generate birch chips.
+  BirchBarView();
+  BirchBarView(const BirchBarView&) = delete;
+  BirchBarView& operator=(const BirchBarView&) = delete;
+  ~BirchBarView() override;
+
+  // Note: these are helper functions for test use.
+  static void ShowWidgetForTesting(std::unique_ptr<BirchBarView> bar_view);
+  static void HideWidgetForTesting();
+
+  // Adds a new birch chip to the bar.
+  // TODO(zxdan): move the function to private when using model and replace the
+  // arguments with chip data structure.
+  void AddChip(const ui::ImageModel& icon,
+               const std::u16string& title,
+               const std::u16string& sub_title,
+               views::Button::PressedCallback callback,
+               std::optional<std::u16string> button_title = std::nullopt,
+               std::optional<views::Button::PressedCallback> button_callback =
+                   std::nullopt);
+
+  // views::View:
+  gfx::Size CalculatePreferredSize() const override;
+  int GetHeightForWidth(int width) const override;
+  void Layout() override;
+
+  // BirchChipButton::Delegate:
+  void RemoveChip(BirchChipButton* chip) override;
+
+ private:
+  class BirchChipsContainer;
+
+  void OnAnimationsEnded(bool show);
+  void OnShowHideChipsButtonPressed(bool show);
+
+  // The container of the birch chips with the hiding chips button.
+  raw_ptr<BirchChipsContainer> chips_container_ = nullptr;
+  // A view contains the show chips button. To sync the scaling and opacity
+  // animations of the show chips button and its blurred background shield
+  // (which is stacked below the button's layer during animation), we set the
+  // button in this container view and animate the container instead of the
+  // button.
+  raw_ptr<views::View> show_chips_button_container_ = nullptr;
+  // Indicating whether there is a showing/hiding animation in progress.
+  bool animation_in_progress_ = false;
+};
+
+}  // namespace ash
+
+#endif  // ASH_WM_OVERVIEW_BIRCH_BIRCH_BAR_VIEW_H_
diff --git a/ash/wm/overview/glanceables/glanceables_chip_button.cc b/ash/wm/overview/birch/birch_chip_button.cc
similarity index 87%
rename from ash/wm/overview/glanceables/glanceables_chip_button.cc
rename to ash/wm/overview/birch/birch_chip_button.cc
index e2a9cb1..97602ecf 100644
--- a/ash/wm/overview/glanceables/glanceables_chip_button.cc
+++ b/ash/wm/overview/birch/birch_chip_button.cc
@@ -1,8 +1,8 @@
-// Copyright 2023 The Chromium Authors
+// Copyright 2024 The Chromium Authors
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "ash/wm/overview/glanceables/glanceables_chip_button.h"
+#include "ash/wm/overview/birch/birch_chip_button.h"
 
 #include "ash/strings/grit/ash_strings.h"
 #include "ash/style/pill_button.h"
@@ -57,9 +57,9 @@
 }  // namespace
 
 //------------------------------------------------------------------------------
-// GlanceablesChipButton::RemovalChipMenuController:
+// BirchChipButton::RemovalChipMenuController:
 // The removal chip panel which contains one option to remove the chip.
-class GlanceablesChipButton::RemovalChipMenuController
+class BirchChipButton::RemovalChipMenuController
     : public views::ContextMenuController {
  public:
   explicit RemovalChipMenuController(ui::SimpleMenuModel::Delegate* delegate)
@@ -97,8 +97,8 @@
 };
 
 //------------------------------------------------------------------------------
-// GlanceablesChipButton:
-GlanceablesChipButton::GlanceablesChipButton()
+// BirchChipButton:
+BirchChipButton::BirchChipButton()
     : removal_chip_menu_controller_(
           std::make_unique<RemovalChipMenuController>(this)) {
   auto box_layout = std::make_unique<views::BoxLayout>(
@@ -123,7 +123,7 @@
           kBackgroundColorId, kRoundedCornerRadius))
       .SetPreferredSize(kChipSize)
       // TODO(zxdan): verbalize all the contents in following changes.
-      .SetAccessibleName(u"Glanceables Chip")
+      .SetAccessibleName(u"Birch Chip")
       .AddChildren(
           // Icon.
           views::Builder<views::ImageView>().CopyAddressTo(&icon_).SetProperty(
@@ -162,35 +162,34 @@
   StyleUtil::SetUpFocusRingForView(this);
 }
 
-GlanceablesChipButton::~GlanceablesChipButton() = default;
+BirchChipButton::~BirchChipButton() = default;
 
-void GlanceablesChipButton::SetIconImage(const ui::ImageModel& icon_image) {
+void BirchChipButton::SetIconImage(const ui::ImageModel& icon_image) {
   icon_->SetImage(icon_image);
 }
 
-void GlanceablesChipButton::SetTitleText(const std::u16string& title) {
+void BirchChipButton::SetTitleText(const std::u16string& title) {
   title_->SetText(title);
 }
 
-void GlanceablesChipButton::SetSubtitleText(const std::u16string& subtitle) {
+void BirchChipButton::SetSubtitleText(const std::u16string& subtitle) {
   subtitle_->SetText(subtitle);
 }
 
-void GlanceablesChipButton::SetActionButton(
-    const std::u16string& label,
-    views::Button::PressedCallback action) {
+void BirchChipButton::SetActionButton(const std::u16string& label,
+                                      views::Button::PressedCallback action) {
   CHECK(!action_button_);
   box_layout_->set_inside_border_insets(kBorderInsetsWithActionButton);
   action_button_ = AddChildView(std::make_unique<PillButton>(
       std::move(action), label, PillButton::Type::kPrimaryWithoutIcon));
 }
 
-void GlanceablesChipButton::SetDelegate(Delegate* delegate) {
+void BirchChipButton::SetDelegate(Delegate* delegate) {
   CHECK(!delegate_);
   delegate_ = delegate;
 }
 
-void GlanceablesChipButton::OnGestureEvent(ui::GestureEvent* event) {
+void BirchChipButton::OnGestureEvent(ui::GestureEvent* event) {
   if (event->type() == ui::ET_GESTURE_LONG_PRESS) {
     // Show removal chip panel.
     gfx::Point screen_location(event->location());
@@ -200,18 +199,18 @@
   }
 }
 
-void GlanceablesChipButton::ExecuteCommand(int command_id, int event_flags) {
+void BirchChipButton::ExecuteCommand(int command_id, int event_flags) {
   // Remove the chip when the option is selected in the removal panel.
   OnRemoveComponentPressed();
 }
 
-void GlanceablesChipButton::OnRemoveComponentPressed() {
+void BirchChipButton::OnRemoveComponentPressed() {
   if (delegate_) {
     delegate_->RemoveChip(this);
   }
 }
 
-BEGIN_METADATA(GlanceablesChipButton, views::Button)
+BEGIN_METADATA(BirchChipButton, views::Button)
 END_METADATA
 
 }  // namespace ash
diff --git a/ash/wm/overview/glanceables/glanceables_chip_button.h b/ash/wm/overview/birch/birch_chip_button.h
similarity index 71%
rename from ash/wm/overview/glanceables/glanceables_chip_button.h
rename to ash/wm/overview/birch/birch_chip_button.h
index 33efafa..3d7e4f4 100644
--- a/ash/wm/overview/glanceables/glanceables_chip_button.h
+++ b/ash/wm/overview/birch/birch_chip_button.h
@@ -1,9 +1,9 @@
-// Copyright 2023 The Chromium Authors
+// Copyright 2024 The Chromium Authors
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef ASH_WM_OVERVIEW_GLANCEABLES_GLANCEABLES_CHIP_BUTTON_H_
-#define ASH_WM_OVERVIEW_GLANCEABLES_GLANCEABLES_CHIP_BUTTON_H_
+#ifndef ASH_WM_OVERVIEW_BIRCH_BIRCH_CHIP_BUTTON_H_
+#define ASH_WM_OVERVIEW_BIRCH_BIRCH_CHIP_BUTTON_H_
 
 #include "ui/base/metadata/metadata_header_macros.h"
 #include "ui/base/models/image_model.h"
@@ -23,24 +23,24 @@
 
 // A compact view of an app, displaying its icon, name, a brief description, and
 // an optional call to action.
-class GlanceablesChipButton : public views::Button,
-                              public ui::SimpleMenuModel::Delegate {
+class BirchChipButton : public views::Button,
+                        public ui::SimpleMenuModel::Delegate {
  public:
-  METADATA_HEADER(GlanceablesChipButton);
+  METADATA_HEADER(BirchChipButton);
 
   // The delegate executes the actions when the chip is removed.
   class Delegate {
    public:
-    virtual void RemoveChip(GlanceablesChipButton* chip) = 0;
+    virtual void RemoveChip(BirchChipButton* chip) = 0;
 
    protected:
     virtual ~Delegate() = default;
   };
 
-  GlanceablesChipButton();
-  GlanceablesChipButton(const GlanceablesChipButton&) = delete;
-  GlanceablesChipButton& operator=(const GlanceablesChipButton&) = delete;
-  ~GlanceablesChipButton() override;
+  BirchChipButton();
+  BirchChipButton(const BirchChipButton&) = delete;
+  BirchChipButton& operator=(const BirchChipButton&) = delete;
+  ~BirchChipButton() override;
 
   // Chip configuration methods.
   void SetIconImage(const ui::ImageModel& icon_image);
@@ -76,11 +76,11 @@
   std::unique_ptr<RemovalChipMenuController> removal_chip_menu_controller_;
 };
 
-BEGIN_VIEW_BUILDER(/*no export*/, GlanceablesChipButton, views::Button)
+BEGIN_VIEW_BUILDER(/*no export*/, BirchChipButton, views::Button)
 VIEW_BUILDER_PROPERTY(const ui::ImageModel&, IconImage)
 VIEW_BUILDER_PROPERTY(const std::u16string&, TitleText)
 VIEW_BUILDER_PROPERTY(const std::u16string&, SubtitleText)
-VIEW_BUILDER_PROPERTY(GlanceablesChipButton::Delegate*, Delegate)
+VIEW_BUILDER_PROPERTY(BirchChipButton::Delegate*, Delegate)
 VIEW_BUILDER_METHOD(SetActionButton,
                     const std::u16string&,
                     views::Button::PressedCallback)
@@ -88,6 +88,6 @@
 
 }  // namespace ash
 
-DEFINE_VIEW_BUILDER(/*no export*/, ash::GlanceablesChipButton)
+DEFINE_VIEW_BUILDER(/*no export*/, ash::BirchChipButton)
 
-#endif  // ASH_WM_OVERVIEW_GLANCEABLES_GLANCEABLES_CHIP_BUTTON_H_
+#endif  // ASH_WM_OVERVIEW_BIRCH_BIRCH_CHIP_BUTTON_H_
diff --git a/ash/wm/overview/glanceables/glanceables_bar_view.h b/ash/wm/overview/glanceables/glanceables_bar_view.h
deleted file mode 100644
index c5d2f03..0000000
--- a/ash/wm/overview/glanceables/glanceables_bar_view.h
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright 2023 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef ASH_WM_OVERVIEW_GLANCEABLES_GLANCEABLES_BAR_VIEW_H_
-#define ASH_WM_OVERVIEW_GLANCEABLES_GLANCEABLES_BAR_VIEW_H_
-
-#include "ash/wm/overview/glanceables/glanceables_chip_button.h"
-#include "ui/base/metadata/metadata_header_macros.h"
-#include "ui/base/models/image_model.h"
-#include "ui/views/controls/button/button.h"
-#include "ui/views/view.h"
-
-namespace ash {
-
-class IconButton;
-
-// The bar container to show/hide glanceable chips. The glanceables chips will
-// be shown in a row with a hiding chips button at the end. When pressing the
-// hiding button, the glanceables chips will fade out and the showing chips
-// button will appear in the center.
-class GlanceablesBarView : public views::View,
-                           public GlanceablesChipButton::Delegate {
-  METADATA_HEADER(GlanceablesBarView, views::View)
-
- public:
-  // TODO(zxdan): When the data model is implemented, pass in the model to
-  // generate glanceable chips.
-  GlanceablesBarView();
-  GlanceablesBarView(const GlanceablesBarView&) = delete;
-  GlanceablesBarView& operator=(const GlanceablesBarView&) = delete;
-  ~GlanceablesBarView() override;
-
-  // Note: these are helper functions for test use.
-  static void ShowWidgetForTesting(
-      std::unique_ptr<GlanceablesBarView> bar_view);
-  static void HideWidgetForTesting();
-
-  // Adds a new glanceable chip to the bar.
-  // TODO(zxdan): move the function to private when using model and replace the
-  // arguments with chip data structure.
-  void AddChip(const ui::ImageModel& icon,
-               const std::u16string& title,
-               const std::u16string& sub_title,
-               views::Button::PressedCallback callback,
-               std::optional<std::u16string> button_title = std::nullopt,
-               std::optional<views::Button::PressedCallback> button_callback =
-                   std::nullopt);
-
-  // views::View:
-  gfx::Size CalculatePreferredSize() const override;
-  int GetHeightForWidth(int width) const override;
-  void Layout() override;
-
-  // GlanceablesChipButton::Delegate:
-  void RemoveChip(GlanceablesChipButton* chip) override;
-
- private:
-  class GlanceablesChipsContainer;
-
-  void OnAnimationsEnded(bool show);
-  void OnShowHideChipsButtonPressed(bool show);
-
-  // The container of the glanceables chips with the hiding chips button.
-  raw_ptr<GlanceablesChipsContainer> chips_container_ = nullptr;
-  // A view contains the show chips button. To sync the scaling and opacity
-  // animations of the show chips button and its blurred background shield
-  // (which is stacked below the button's layer during animation), we set the
-  // button in this container view and animate the container instead of the
-  // button.
-  raw_ptr<views::View> show_chips_button_container_ = nullptr;
-  // Indicating whether there is a showing/hiding animation in progress.
-  bool animation_in_progress_ = false;
-};
-
-}  // namespace ash
-
-#endif  // ASH_WM_OVERVIEW_GLANCEABLES_GLANCEABLES_BAR_VIEW_H_
diff --git a/ash/wm/tablet_mode/tablet_mode_multitask_menu_controller.cc b/ash/wm/tablet_mode/tablet_mode_multitask_menu_controller.cc
index 15d6d25..c3dd9be2 100644
--- a/ash/wm/tablet_mode/tablet_mode_multitask_menu_controller.cc
+++ b/ash/wm/tablet_mode/tablet_mode_multitask_menu_controller.cc
@@ -94,7 +94,7 @@
   if (is_drag_active_) {
     if (!reserved_for_gesture_sent_) {
       reserved_for_gesture_sent_ = true;
-      event->set_flags(event->flags() | ui::EF_RESERVED_FOR_GESTURE);
+      event->SetFlags(event->flags() | ui::EF_RESERVED_FOR_GESTURE);
       return;
     }
     event->StopPropagation();
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index ad16fe23..7d3d1bc7 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -2872,6 +2872,17 @@
     {"Quick Settings Nudge", kCaptureModeEducationQuickSettingsNudge,
      std::size(kCaptureModeEducationQuickSettingsNudge), nullptr}};
 
+const FeatureEntry::FeatureParam
+    kHoldingSpaceWallpaperNudgeDropToPinDisabled[] = {{"drop-to-pin", "false"}};
+const FeatureEntry::FeatureParam kHoldingSpaceWallpaperNudgeDropToPinEnabled[] =
+    {{"drop-to-pin", "true"}};
+const FeatureEntry::FeatureVariation kHoldingSpaceWallpaperNudgeVariations[] = {
+    {"with drop-to-pin", kHoldingSpaceWallpaperNudgeDropToPinEnabled,
+     std::size(kHoldingSpaceWallpaperNudgeDropToPinEnabled), nullptr},
+    {"without drop-to-pin", kHoldingSpaceWallpaperNudgeDropToPinDisabled,
+     std::size(kHoldingSpaceWallpaperNudgeDropToPinDisabled), nullptr},
+};
+
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
 #if BUILDFLAG(IS_CHROMEOS)
@@ -9571,14 +9582,6 @@
      FEATURE_VALUE_TYPE(features::kRequestDesktopSiteZoom)},
 #endif  // BUILDFLAG(IS_ANDROID)
 
-#if !BUILDFLAG(IS_ANDROID)
-    {"enable-web-hid-on-extension-service-worker",
-     flag_descriptions::kEnableWebHidOnExtensionServiceWorkerName,
-     flag_descriptions::kEnableWebHidOnExtensionServiceWorkerDescription,
-     kOsDesktop,
-     FEATURE_VALUE_TYPE(features::kEnableWebHidOnExtensionServiceWorker)},
-#endif
-
     {"autofill-enable-remade-downstream-metrics",
      flag_descriptions::kAutofillEnableRemadeDownstreamMetricsName,
      flag_descriptions::kAutofillEnableRemadeDownstreamMetricsDescription,
@@ -9754,11 +9757,6 @@
      flag_descriptions::kUseDMSAAForTilesAndroidGLDescription, kOsAndroid,
      FEATURE_VALUE_TYPE(::features::kUseDMSAAForTilesAndroidGL)},
 #endif
-    {"enable-web-usb-on-extension-service-worker",
-     flag_descriptions::kEnableWebUsbOnExtensionServiceWorkerName,
-     flag_descriptions::kEnableWebUsbOnExtensionServiceWorkerDescription,
-     kOsAndroid | kOsDesktop,
-     FEATURE_VALUE_TYPE(features::kEnableWebUsbOnExtensionServiceWorker)},
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
     {"enable-holding-space-predictability",
@@ -9773,6 +9771,18 @@
      flag_descriptions::kHoldingSpaceSuggestionsName,
      flag_descriptions::kHoldingSpaceSuggestionsDescription, kOsCrOS,
      FEATURE_VALUE_TYPE(ash::features::kHoldingSpaceSuggestions)},
+    {"enable-holding-space-wallpaper-nudge",
+     flag_descriptions::kHoldingSpaceWallpaperNudgeName,
+     flag_descriptions::kHoldingSpaceWallpaperNudgeDescription, kOsCrOS,
+     FEATURE_WITH_PARAMS_VALUE_TYPE(ash::features::kHoldingSpaceWallpaperNudge,
+                                    kHoldingSpaceWallpaperNudgeVariations,
+                                    "HoldingSpaceWallpaperNudge")},
+    {"enable-holding-space-wallpaper-nudge-force-eligibility",
+     flag_descriptions::kHoldingSpaceWallpaperNudgeForceEligibilityName,
+     flag_descriptions::kHoldingSpaceWallpaperNudgeForceEligibilityDescription,
+     kOsCrOS,
+     FEATURE_VALUE_TYPE(
+         ash::features::kHoldingSpaceWallpaperNudgeForceEligibility)},
     {"enable-welcome-tour", flag_descriptions::kWelcomeTourName,
      flag_descriptions::kWelcomeTourDescription, kOsCrOS,
      FEATURE_VALUE_TYPE(ash::features::kWelcomeTour)},
diff --git a/chrome/browser/ash/events/event_rewriter_unittest.cc b/chrome/browser/ash/events/event_rewriter_unittest.cc
index 888a2e0..7faebfc5 100644
--- a/chrome/browser/ash/events/event_rewriter_unittest.cc
+++ b/chrome/browser/ash/events/event_rewriter_unittest.cc
@@ -3346,7 +3346,7 @@
   ui::TouchEvent press(
       ui::ET_TOUCH_PRESSED, location, base::TimeTicks(),
       ui::PointerDetails(ui::EventPointerType::kTouch, kTouchId));
-  press.set_flags(ui::EF_CONTROL_DOWN);
+  press.SetFlags(ui::EF_CONTROL_DOWN);
 
   source().Send(&press);
   auto events = TakeEvents();
diff --git a/chrome/browser/component_updater/chrome_component_updater_configurator.cc b/chrome/browser/component_updater/chrome_component_updater_configurator.cc
index 47a3814..c1a4486 100644
--- a/chrome/browser/component_updater/chrome_component_updater_configurator.cc
+++ b/chrome/browser/component_updater/chrome_component_updater_configurator.cc
@@ -12,6 +12,7 @@
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/scoped_refptr.h"
 #include "base/path_service.h"
 #include "base/strings/sys_string_conversions.h"
 #include "base/time/time.h"
@@ -88,6 +89,7 @@
   absl::optional<bool> IsMachineExternallyManaged() const override;
   update_client::UpdaterStateProvider GetUpdaterStateProvider() const override;
   absl::optional<base::FilePath> GetCrxCachePath() const override;
+  bool IsConnectionMetered() const override;
 
  private:
   friend class base::RefCountedThreadSafe<ChromeConfigurator>;
@@ -282,6 +284,10 @@
                 : absl::nullopt;
 }
 
+bool ChromeConfigurator::IsConnectionMetered() const {
+  return configurator_impl_.IsConnectionMetered();
+}
+
 }  // namespace
 
 scoped_refptr<update_client::Configurator>
diff --git a/chrome/browser/component_updater/payload_test_component_installer.cc b/chrome/browser/component_updater/payload_test_component_installer.cc
index 0f43f64..290ac2e0 100644
--- a/chrome/browser/component_updater/payload_test_component_installer.cc
+++ b/chrome/browser/component_updater/payload_test_component_installer.cc
@@ -92,6 +92,11 @@
   return false;
 }
 
+bool PayloadTestComponentInstallerPolicy::AllowUpdatesOnMeteredConnections()
+    const {
+  return false;
+}
+
 void RegisterPayloadTestComponent(ComponentUpdateService* cus) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
   base::MakeRefCounted<ComponentInstaller>(
diff --git a/chrome/browser/component_updater/payload_test_component_installer.h b/chrome/browser/component_updater/payload_test_component_installer.h
index 4ea82d2e..b75b6fb9 100644
--- a/chrome/browser/component_updater/payload_test_component_installer.h
+++ b/chrome/browser/component_updater/payload_test_component_installer.h
@@ -33,6 +33,7 @@
   std::string GetName() const override;
   update_client::InstallerAttributes GetInstallerAttributes() const override;
   bool AllowCachedCopies() const override;
+  bool AllowUpdatesOnMeteredConnections() const override;
 };
 
 void RegisterPayloadTestComponent(ComponentUpdateService* cus);
diff --git a/chrome/browser/component_updater/recovery_component_installer.cc b/chrome/browser/component_updater/recovery_component_installer.cc
index 803f77b..3c40d30 100644
--- a/chrome/browser/component_updater/recovery_component_installer.cc
+++ b/chrome/browser/component_updater/recovery_component_installer.cc
@@ -97,13 +97,15 @@
 
   // Add a flag for re-attempted install with elevated privilege so that the
   // recovery executable can report back accordingly.
-  if (is_deferred_run)
+  if (is_deferred_run) {
     arguments.push_back("/deferredrun");
+  }
 
   if (const std::string* recovery_args =
           manifest.FindString("x-recovery-args")) {
-    if (base::IsStringASCII(*recovery_args))
+    if (base::IsStringASCII(*recovery_args)) {
       arguments.push_back(*recovery_args);
+    }
   }
   if (const std::string* recovery_add_version =
           manifest.FindString("x-recovery-add-version")) {
@@ -123,10 +125,11 @@
     const base::Version& version) {
   base::CommandLine command_line(command);
 
-  const auto arguments = GetRecoveryInstallArguments(
-      manifest, is_deferred_run, version);
-  for (const auto& arg : arguments)
+  const auto arguments =
+      GetRecoveryInstallArguments(manifest, is_deferred_run, version);
+  for (const auto& arg : arguments) {
     command_line.AppendArg(arg);
+  }
 
   return command_line;
 }
@@ -146,21 +149,25 @@
   const base::FilePath main_file = path.Append(kRecoveryFileName);
   const base::FilePath manifest_file =
       path.Append(FILE_PATH_LITERAL("manifest.json"));
-  if (!base::PathExists(main_file) || !base::PathExists(manifest_file))
+  if (!base::PathExists(main_file) || !base::PathExists(manifest_file)) {
     return;
+  }
 
   base::Value::Dict manifest(ReadManifest(manifest_file));
   const std::string* name = manifest.FindString("name");
-  if (!name || *name != kRecoveryManifestName)
+  if (!name || *name != kRecoveryManifestName) {
     return;
+  }
   std::string proposed_version;
   if (const std::string* ptr = manifest.FindString("version")) {
-    if (base::IsStringASCII(*ptr))
+    if (base::IsStringASCII(*ptr)) {
       proposed_version = *ptr;
+    }
   }
   const base::Version version(proposed_version);
-  if (!version.IsValid())
+  if (!version.IsValid()) {
     return;
+  }
 
   const bool is_deferred_run = true;
 #if BUILDFLAG(IS_WIN)
@@ -184,8 +191,9 @@
   // ExecuteWithPrivilegesAndGetPID(): an array of string pointers
   // that ends with a null pointer.
   std::vector<const char*> raw_string_args;
-  for (const auto& arg : arguments)
+  for (const auto& arg : arguments) {
     raw_string_args.push_back(arg.c_str());
+  }
   raw_string_args.push_back(nullptr);
 
   pid_t pid = -1;
@@ -278,14 +286,19 @@
                          std::end(kRecoverySha2Hash));
   if (!cus->RegisterComponent(ComponentRegistration(
           update_client::GetCrxIdFromPublicKeyHash(public_key_hash), "recovery",
-          public_key_hash, version, {}, {}, nullptr,
-          new RecoveryComponentInstaller(version, prefs), false, true, true))) {
+          public_key_hash, version, /*fingerprint=*/{},
+          /*installer_attributes=*/{}, /*action_handler=*/nullptr,
+          new RecoveryComponentInstaller(version, prefs),
+          /*requires_network_encryption=*/false,
+          /*supports_group_policy_enable_component_updates=*/true,
+          /*allow_cached_copies=*/true,
+          /*allow_updates_on_metered_connection=*/true))) {
     NOTREACHED() << "Recovery component registration failed.";
   }
 }
 
-void RecoveryUpdateVersionHelper(
-    const base::Version& version, PrefService* prefs) {
+void RecoveryUpdateVersionHelper(const base::Version& version,
+                                 PrefService* prefs) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
   prefs->SetString(prefs::kRecoveryComponentVersion, version.GetString());
 }
@@ -298,7 +311,8 @@
 }
 
 RecoveryComponentInstaller::RecoveryComponentInstaller(
-    const base::Version& version, PrefService* prefs)
+    const base::Version& version,
+    PrefService* prefs)
     : current_version_(version), prefs_(prefs) {
   DCHECK(version.IsValid());
 }
@@ -329,8 +343,9 @@
   options.start_hidden = true;
 #endif
   base::Process process = base::LaunchProcess(cmdline, options);
-  if (!process.IsValid())
+  if (!process.IsValid()) {
     return false;
+  }
 
   // Let worker pool thread wait for us so we don't block Chrome shutdown.
   // This task joins a process, hence .WithBaseSyncPrimitives().
@@ -351,13 +366,15 @@
 // Sets the POSIX executable permissions on a file
 bool SetPosixExecutablePermission(const base::FilePath& path) {
   int permissions = 0;
-  if (!base::GetPosixFilePermissions(path, &permissions))
+  if (!base::GetPosixFilePermissions(path, &permissions)) {
     return false;
+  }
   const int kExecutableMask = base::FILE_PERMISSION_EXECUTE_BY_USER |
                               base::FILE_PERMISSION_EXECUTE_BY_GROUP |
                               base::FILE_PERMISSION_EXECUTE_BY_OTHERS;
-  if ((permissions & kExecutableMask) == kExecutableMask)
+  if ((permissions & kExecutableMask) == kExecutableMask) {
     return true;  // No need to update
+  }
   return base::SetPosixFilePermissions(path, permissions | kExecutableMask);
 }
 #endif  // BUILDFLAG(IS_POSIX)
diff --git a/chrome/browser/extensions/updater/chrome_update_client_config.cc b/chrome/browser/extensions/updater/chrome_update_client_config.cc
index b94e0bbe..a9304f0 100644
--- a/chrome/browser/extensions/updater/chrome_update_client_config.cc
+++ b/chrome/browser/extensions/updater/chrome_update_client_config.cc
@@ -16,6 +16,7 @@
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/scoped_refptr.h"
 #include "base/no_destructor.h"
 #include "base/path_service.h"
 #include "base/scoped_observation.h"
@@ -201,14 +202,16 @@
 }
 
 std::vector<GURL> ChromeUpdateClientConfig::UpdateUrl() const {
-  if (url_override_.has_value())
+  if (url_override_.has_value()) {
     return {*url_override_};
+  }
   return impl_.UpdateUrl();
 }
 
 std::vector<GURL> ChromeUpdateClientConfig::PingUrl() const {
-  if (url_override_.has_value())
+  if (url_override_.has_value()) {
     return {*url_override_};
+  }
   return impl_.PingUrl();
 }
 
@@ -297,8 +300,9 @@
 }
 
 bool ChromeUpdateClientConfig::EnabledCupSigning() const {
-  if (url_override_.has_value())
+  if (url_override_.has_value()) {
     return false;
+  }
   return impl_.EnabledCupSigning();
 }
 
@@ -356,4 +360,8 @@
                 : absl::nullopt;
 }
 
+bool ChromeUpdateClientConfig::IsConnectionMetered() const {
+  return impl_.IsConnectionMetered();
+}
+
 }  // namespace extensions
diff --git a/chrome/browser/extensions/updater/chrome_update_client_config.h b/chrome/browser/extensions/updater/chrome_update_client_config.h
index 43222dd..a0f1bc5 100644
--- a/chrome/browser/extensions/updater/chrome_update_client_config.h
+++ b/chrome/browser/extensions/updater/chrome_update_client_config.h
@@ -13,6 +13,7 @@
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/ref_counted.h"
+#include "base/memory/scoped_refptr.h"
 #include "base/time/time.h"
 #include "components/component_updater/configurator_impl.h"
 #include "components/update_client/configurator.h"
@@ -84,6 +85,7 @@
   absl::optional<bool> IsMachineExternallyManaged() const override;
   update_client::UpdaterStateProvider GetUpdaterStateProvider() const override;
   absl::optional<base::FilePath> GetCrxCachePath() const override;
+  bool IsConnectionMetered() const override;
 
  protected:
   friend class base::RefCountedThreadSafe<ChromeUpdateClientConfig>;
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index c03033b..36bcb47 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -2896,6 +2896,16 @@
     "expiry_milestone": 123
   },
   {
+    "name": "enable-holding-space-wallpaper-nudge",
+    "owners": [ "//ash/user_education/OWNERS" ],
+    "expiry_milestone": 123
+  },
+  {
+    "name": "enable-holding-space-wallpaper-nudge-force-eligibility",
+    "owners": [ "//ash/user_education/OWNERS" ],
+    "expiry_milestone": 123
+  },
+  {
     "name": "enable-hostname-setting",
     "owners": [ "khorimoto@chromium.org", "cros-connectivity@google.com" ],
     "expiry_milestone": 125
@@ -3721,11 +3731,6 @@
     "expiry_milestone": 150
   },
   {
-    "name": "enable-web-hid-on-extension-service-worker",
-    "owners": [ "deviceapi-team@google.com" ],
-    "expiry_milestone": 120
-  },
-  {
     "name": "enable-web-payments-experimental-features",
     "owners": [ "rouslan@chromium.org", "web-payments-team@google.com" ],
     // This flag is used by early adoption partners to test new Web Payments
@@ -3733,11 +3738,6 @@
     "expiry_milestone": -1
   },
   {
-    "name": "enable-web-usb-on-extension-service-worker",
-    "owners": [ "deviceapi-team@google.com" ],
-    "expiry_milestone": 120
-  },
-  {
     "name": "enable-webassembly-baseline",
     "owners": [ "clemensb@chromium.org", "wasm-team@google.com" ],
     // This flag is often used by developers and partners to test
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 4045bcd..1d49e39 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -1333,11 +1333,6 @@
     "When enabled, Chrome will attempt to connect to the system tracing "
     "service";
 
-const char kEnableWebUsbOnExtensionServiceWorkerName[] =
-    "Enable WebUSB on extension service workers";
-const char kEnableWebUsbOnExtensionServiceWorkerDescription[] =
-    "When enabled, WebUSB API is available on extension service workers.";
-
 const char kEnableWindowsGamingInputDataFetcherName[] =
     "Enable Windows.Gaming.Input";
 const char kEnableWindowsGamingInputDataFetcherDescription[] =
@@ -4659,11 +4654,6 @@
 const char kReadAnythingLocalSidePanelDescription[] =
     "Keep the Reading Mode side panel separate and local to each tab.";
 
-const char kEnableWebHidOnExtensionServiceWorkerName[] =
-    "Enable WebHID on extension service workers";
-const char kEnableWebHidOnExtensionServiceWorkerDescription[] =
-    "When enabled, WebHID API is available on extension service workers.";
-
 const char kGlobalMediaControlsCastStartStopName[] =
     "Global media controls control Cast start/stop";
 const char kGlobalMediaControlsCastStartStopDescription[] =
@@ -6496,6 +6486,21 @@
     "Enables pinned file suggestions in holding space to help the user "
     "understand and discover the ability to pin.";
 
+const char kHoldingSpaceWallpaperNudgeName[] =
+    "Enable holding space wallpaper nudge";
+const char kHoldingSpaceWallpaperNudgeDescription[] =
+    "Enables the nudge that educates the user on holding space pinning "
+    "behavior when the user drags a file over the wallpaper.";
+
+const char kHoldingSpaceWallpaperNudgeForceEligibilityName[] =
+    "Force eligibility for holding space wallpaper nudge";
+const char kHoldingSpaceWallpaperNudgeForceEligibilityDescription[] =
+    "Ignores the rate limits and user type limits put on the holding space "
+    "wallpaper nudge, meaning it will show whenever a user takes a triggering "
+    "action, regardless of how much or how recently it has been shown, user "
+    "new-ness, or account type. Enabling this flag has no effect unless the "
+    "holding space wallpaper nudge is enabled.";
+
 const char kHotspotName[] = "Hotspot";
 const char kHotspotDescription[] =
     "Enables the Chromebook to share its cellular internet connection to other "
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 4cb4316..3c16b105 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -671,9 +671,6 @@
 extern const char kEnableNetworkLoggingToFileName[];
 extern const char kEnableNetworkLoggingToFileDescription[];
 
-extern const char kEnableWebUsbOnExtensionServiceWorkerName[];
-extern const char kEnableWebUsbOnExtensionServiceWorkerDescription[];
-
 extern const char kEnableWindowsGamingInputDataFetcherName[];
 extern const char kEnableWindowsGamingInputDataFetcherDescription[];
 
@@ -2695,9 +2692,6 @@
 extern const char kReadAnythingLocalSidePanelName[];
 extern const char kReadAnythingLocalSidePanelDescription[];
 
-extern const char kEnableWebHidOnExtensionServiceWorkerName[];
-extern const char kEnableWebHidOnExtensionServiceWorkerDescription[];
-
 extern const char kGlobalMediaControlsCastStartStopName[];
 extern const char kGlobalMediaControlsCastStartStopDescription[];
 
@@ -3735,6 +3729,12 @@
 extern const char kHoldingSpaceSuggestionsName[];
 extern const char kHoldingSpaceSuggestionsDescription[];
 
+extern const char kHoldingSpaceWallpaperNudgeName[];
+extern const char kHoldingSpaceWallpaperNudgeDescription[];
+
+extern const char kHoldingSpaceWallpaperNudgeForceEligibilityName[];
+extern const char kHoldingSpaceWallpaperNudgeForceEligibilityDescription[];
+
 extern const char kHotspotName[];
 extern const char kHotspotDescription[];
 
diff --git a/chrome/browser/policy/test/component_updater_policy_browsertest.cc b/chrome/browser/policy/test/component_updater_policy_browsertest.cc
index a849fa7..1a81d8d 100644
--- a/chrome/browser/policy/test/component_updater_policy_browsertest.cc
+++ b/chrome/browser/policy/test/component_updater_policy_browsertest.cc
@@ -171,8 +171,12 @@
   // The component uses HTTPS only for network interception purposes.
   return component_updater::ComponentRegistration(
       "jebgalgnebhfojomionfpkfelancnnkf", {}, jebg_hash, base::Version("0.9"),
-      {}, {}, nullptr, base::MakeRefCounted<MockInstaller>(), true,
-      supports_group_policy_enable_component_updates, true);
+      /*fingerprint=*/{}, /*installer_attributes=*/{},
+      /*action_handler=*/nullptr, base::MakeRefCounted<MockInstaller>(),
+      /*requires_network_encryption=*/true,
+      supports_group_policy_enable_component_updates,
+      /*allow_cached_copies=*/true,
+      /*allow_updates_on_metered_connection=*/true);
 }
 
 void ComponentUpdaterPolicyTest::UpdateComponent(
diff --git a/chrome/browser/resources/ash/settings/css/base.css b/chrome/browser/resources/ash/settings/css/base.css
index 53b47fa..becc64fa 100644
--- a/chrome/browser/resources/ash/settings/css/base.css
+++ b/chrome/browser/resources/ash/settings/css/base.css
@@ -22,7 +22,7 @@
 }
 
 html {
-  background-color: var(--settings-base-bg-color);
+  background-color: var(--cros-sys-app_base_shaded);
   overflow: hidden;
   /* Remove 300ms delay for 'click' event, when using touch interface. */
   touch-action: manipulation;
diff --git a/chrome/browser/resources/ash/settings/main_page_container/main_page_container.html b/chrome/browser/resources/ash/settings/main_page_container/main_page_container.html
index eb702ff7..4adb06c 100644
--- a/chrome/browser/resources/ash/settings/main_page_container/main_page_container.html
+++ b/chrome/browser/resources/ash/settings/main_page_container/main_page_container.html
@@ -11,6 +11,10 @@
     display: none;
   }
 
+  :host-context(body.revamp-wayfinding-enabled):host(:not(.showing-subpage)) {
+    padding-top: 8px;
+  }
+
   .banner {
     align-items: center;
     background-color: var(--cros-bg-color);
diff --git a/chrome/browser/resources/ash/settings/os_settings_page/os_settings_animated_pages.html b/chrome/browser/resources/ash/settings/os_settings_page/os_settings_animated_pages.html
index 68664baf..46e35ef 100644
--- a/chrome/browser/resources/ash/settings/os_settings_page/os_settings_animated_pages.html
+++ b/chrome/browser/resources/ash/settings/os_settings_page/os_settings_animated_pages.html
@@ -1,16 +1,3 @@
-<style>
-  /* L1 page content backdrop style */
-  :host-context(body.revamp-wayfinding-enabled)
-      ::slotted(div[route-path="default"]) {
-    background-color: var(--settings-content-backdrop-bg-color);
-    border-radius: 20px;
-    padding-bottom: 16px;
-    padding-inline-end: 16px;
-    padding-inline-start: 16px;
-    padding-top: 8px;
-  }
-</style>
-
 <iron-pages id="animatedPages" attr-for-selected="route-path"
     on-iron-select="onIronSelect_">
   <slot></slot>
diff --git a/chrome/browser/resources/ash/settings/os_settings_page/settings_card.html b/chrome/browser/resources/ash/settings/os_settings_page/settings_card.html
index 16256c5..86755e25 100644
--- a/chrome/browser/resources/ash/settings/os_settings_page/settings_card.html
+++ b/chrome/browser/resources/ash/settings/os_settings_page/settings_card.html
@@ -1,6 +1,5 @@
 <style>
   :host {
-    --settings-card-bg-color: var(--cros-sys-app_base);
     --settings-card-border-radius: var(--cr-card-border-radius);
 
     display: flex;
@@ -15,12 +14,6 @@
     margin-bottom: 16px;
   }
 
-  @media (prefers-color-scheme: dark) {
-    :host-context(body.revamp-wayfinding-enabled):host {
-      --settings-card-bg-color: var(--cros-sys-surface1);
-    }
-  }
-
   :host-context(body.revamp-wayfinding-enabled) #header {
     margin: 0;
     padding: 8px;
@@ -47,7 +40,7 @@
   }
 
   #card {
-    background-color: var(--settings-card-bg-color);
+    background-color: var(--cros-sys-app_base);
     border-radius: var(--settings-card-border-radius);
     flex: 1;
     overflow: hidden;
diff --git a/chrome/browser/resources/ash/settings/os_settings_ui/os_settings_ui.html b/chrome/browser/resources/ash/settings/os_settings_ui/os_settings_ui.html
index 4ad6aea..4dbd1a28 100644
--- a/chrome/browser/resources/ash/settings/os_settings_ui/os_settings_ui.html
+++ b/chrome/browser/resources/ash/settings/os_settings_ui/os_settings_ui.html
@@ -49,7 +49,7 @@
   }
 
   #left,
-  #center,
+  #main,
   #right {
     flex: 1 1 0;
   }
@@ -66,6 +66,17 @@
     overscroll-behavior: contain;
   }
 
+  :host-context(body.revamp-wayfinding-enabled) #left os-settings-menu {
+    background-color: var(--cros-sys-surface3);
+    border-start-end-radius: 30px;
+  }
+
+  @media (prefers-color-scheme: dark) {
+    :host-context(body.revamp-wayfinding-enabled) #left os-settings-menu {
+      background-color: var(--cros-sys-surface2);
+    }
+  }
+
   :host-context(body.revamp-wayfinding-enabled) #drawer {
     /* TODO(b/316088424) Use window border radius token if available */
     --cr-drawer-border-start-end-radius: 12px;
@@ -76,7 +87,7 @@
     --cr-drawer-width: var(--settings-menu-width);
   }
 
-  #center {
+  #main {
     flex-basis: var(--settings-main-basis);
   }
 
@@ -89,7 +100,7 @@
       display: none;
     }
 
-    #center {
+    #main {
       min-width: auto;
       /* Add some padding to make room for borders and to prevent focus
         * indicators from being cropped. */
@@ -147,7 +158,6 @@
     </div>
   </cr-drawer>
 </template>
-<!-- #container is the shadow container for CrContainerShadowMixin. -->
 <div id="container" class="no-outline">
   <div id="left">
     <template is="dom-if" if="[[showNavMenu_]]">
@@ -158,14 +168,12 @@
       </os-settings-menu>
     </template>
   </div>
-  <div id="center">
-    <os-settings-main
-        prefs="{{prefs}}"
-        toolbar-spinner-active="{{toolbarSpinnerActive_}}"
-        page-availability="[[pageAvailability_]]"
-        advanced-toggle-expanded="{{advancedOpenedInMain_}}">
-    </os-settings-main>
-  </div>
+  <os-settings-main id="main"
+      prefs="{{prefs}}"
+      toolbar-spinner-active="{{toolbarSpinnerActive_}}"
+      page-availability="[[pageAvailability_]]"
+      advanced-toggle-expanded="{{advancedOpenedInMain_}}">
+  </os-settings-main>
   <!-- An additional child of the flex #container to take up space,
         aligned with the right-hand child of the flex toolbar. -->
   <div id="right"></div>
diff --git a/chrome/browser/resources/ash/settings/os_toolbar/os_toolbar.html b/chrome/browser/resources/ash/settings/os_toolbar/os_toolbar.html
index 6ed1a24..5312569 100644
--- a/chrome/browser/resources/ash/settings/os_toolbar/os_toolbar.html
+++ b/chrome/browser/resources/ash/settings/os_toolbar/os_toolbar.html
@@ -1,7 +1,7 @@
 <style include="cr-icons cr-hidden-style settings-shared">
   :host {
     align-items: center;
-    background-color: var(--settings-base-bg-color);
+    background-color: var(--cros-sys-app_base_shaded);
     color: var(--cros-text-color-secondary);
     display: flex;
     height: var(--settings-toolbar-height);
diff --git a/chrome/browser/resources/ash/settings/settings_vars.css b/chrome/browser/resources/ash/settings/settings_vars.css
index 4d2c7971..ac76d40 100644
--- a/chrome/browser/resources/ash/settings/settings_vars.css
+++ b/chrome/browser/resources/ash/settings/settings_vars.css
@@ -24,8 +24,6 @@
 
   --cr-radio-group-item-padding: 0;
 
-  --settings-base-bg-color: var(--cros-sys-app_base_shaded);
-
   --settings-menu-width: 250px;
   --settings-menu-item-border-width: 2px;
 
@@ -45,13 +43,9 @@
 /* TODO(b/302374851) Move these vars into the html block above once the feature
  * is fully launched and can be cleaned up.
  */
-html:has(body.revamp-wayfinding-enabled) {
-  --settings-base-bg-color: var(--cros-sys-surface3);
-
+body.revamp-wayfinding-enabled {
   --settings-container-padding-top: 8px;
 
-  --settings-content-backdrop-bg-color: var(--cros-sys-surface1);
-
   --settings-menu-item-width: 256px;
 
   --settings-menu-padding-inline-end: 16px;
@@ -78,10 +72,4 @@
     --iron-icon-fill-color: var(--google-grey-500);
     --settings-error-color: var(--google-red-300);
   }
-
-  html:has(body.revamp-wayfinding-enabled) {
-    --settings-base-bg-color: var(--cros-sys-app_base_shaded);
-
-    --settings-content-backdrop-bg-color: var(--cros-sys-app_base);
-  }
 }
diff --git a/chrome/browser/resources/chromeos/accessibility/common/BUILD.gn b/chrome/browser/resources/chromeos/accessibility/common/BUILD.gn
index 45acd9b1..9e70c17 100644
--- a/chrome/browser/resources/chromeos/accessibility/common/BUILD.gn
+++ b/chrome/browser/resources/chromeos/accessibility/common/BUILD.gn
@@ -29,7 +29,6 @@
   "async_util.js",
   "automation_predicate.js",
   "browser_util.js",
-  "event_handler.js",
   "gdocs_script.js",
   "key_code.js",
   "local_storage.js",
@@ -48,6 +47,7 @@
   "cursors/range.ts",
   "cursors/recovery_strategy.ts",
   "event_generator.ts",
+  "event_handler.ts",
   "flags.ts",
   "instance_checker.ts",
   "paragraph_utils.ts",
diff --git a/chrome/browser/resources/chromeos/accessibility/common/event_handler.js b/chrome/browser/resources/chromeos/accessibility/common/event_handler.js
deleted file mode 100644
index 3b9987f8..0000000
--- a/chrome/browser/resources/chromeos/accessibility/common/event_handler.js
+++ /dev/null
@@ -1,159 +0,0 @@
-// Copyright 2020 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-const AutomationEvent = chrome.automation.AutomationEvent;
-const AutomationNode = chrome.automation.AutomationNode;
-
-/**
- * This class wraps AutomationNode event listeners, adding some convenience
- * functions.
- */
-export class EventHandler {
-  /**
-   * @param {!AutomationNode | !Array<!AutomationNode>} nodes
-   * @param {!chrome.automation.EventType |
-   *     !Array<!chrome.automation.EventType>} types
-   * @param {?function(!AutomationEvent)} callback
-   * @param {{capture: (boolean|undefined), exactMatch: (boolean|undefined),
-   *     listenOnce: (boolean|undefined), predicate:
-   *     ((function(AutomationEvent): boolean)|undefined)}}
-   *     options
-   *   exactMatch Whether to ignore events where the target is not the provided
-   *       node.
-   *   capture True if the event should be processed before it has reached the
-   *       target node, false if it should be processed after.
-   *   listenOnce True if the event listeners should automatically be removed
-   *       when the callback is called once.
-   *   predicate A predicate for what events will be processed.
-   */
-  constructor(nodes, types, callback, options = {}) {
-    /** @private {!Array<!AutomationNode>} */
-    this.nodes_ = nodes instanceof Array ? nodes : [nodes];
-
-    /** @private {!Array<!chrome.automation.EventType>} */
-    this.types_ = types instanceof Array ? types : [types];
-
-    /** @private {?function(!AutomationEvent)} */
-    this.callback_ = callback;
-
-    /** @private {boolean} */
-    this.capture_ = options.capture || false;
-
-    /** @private {boolean} */
-    this.exactMatch_ = options.exactMatch || false;
-
-    /** @private {boolean} */
-    this.listenOnce_ = options.listenOnce || false;
-
-    /**
-     * Default is a function that always returns true.
-     * @private {!function(AutomationEvent): boolean}
-     */
-    this.predicate_ = options.predicate || (e => true);
-
-    /** @private {boolean} */
-    this.listening_ = false;
-
-    /** @private {!function(AutomationEvent)} */
-    this.handler_ = event => this.handleEvent_(event);
-  }
-
-  /** Starts listening to events. */
-  start() {
-    if (this.listening_) {
-      return;
-    }
-
-    for (const node of this.nodes_) {
-      for (const type of this.types_) {
-        node.addEventListener(type, this.handler_, this.capture_);
-      }
-    }
-    this.listening_ = true;
-  }
-
-  /** Stops listening or handling future events. */
-  stop() {
-    for (const node of this.nodes_) {
-      for (const type of this.types_) {
-        node.removeEventListener(type, this.handler_, this.capture_);
-      }
-    }
-    this.listening_ = false;
-  }
-
-  /**
-   * @return {boolean} Whether this EventHandler is currently listening for
-   *     events.
-   */
-  listening() {
-    return this.listening_;
-  }
-
-  /** @param {?function(!AutomationEvent)} callback */
-  setCallback(callback) {
-    this.callback_ = callback;
-  }
-
-  /**
-   * Changes what nodes are being listened to. Removes listeners from existing
-   *     nodes before adding listeners on new nodes.
-   * @param {!AutomationNode | !Array<!AutomationNode>} nodes
-   */
-  setNodes(nodes) {
-    const wasListening = this.listening_;
-    this.stop();
-    this.nodes_ = nodes instanceof Array ? nodes : [nodes];
-    if (wasListening) {
-      this.start();
-    }
-  }
-
-  /**
-   * Adds another node to the set of nodes being listened to.
-   * @param {!AutomationNode} node
-   */
-  addNode(node) {
-    this.nodes_.push(node);
-
-    if (this.listening_) {
-      for (const type of this.types_) {
-        node.addEventListener(type, this.handler_, this.capture_);
-      }
-    }
-  }
-
-  /**
-   * Removes a specific node from the set of nodes being listened to.
-   * @param {!AutomationNode} node
-   */
-  removeNode(node) {
-    this.nodes_ = this.nodes_.filter(n => n !== node);
-
-    if (this.listening_) {
-      for (const type of this.types_) {
-        node.removeEventListener(type, this.handler_, this.capture_);
-      }
-    }
-  }
-
-  /** @private */
-  handleEvent_(event) {
-    if (this.exactMatch_ && !this.nodes_.includes(event.target)) {
-      return;
-    }
-
-    if (!this.predicate_(event)) {
-      return;
-    }
-
-    if (this.listenOnce_) {
-      this.stop();
-    }
-
-    if (this.callback_) {
-      this.callback_(event);
-    }
-  }
-}
diff --git a/chrome/browser/resources/chromeos/accessibility/common/event_handler.ts b/chrome/browser/resources/chromeos/accessibility/common/event_handler.ts
new file mode 100644
index 0000000..0c38646
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/common/event_handler.ts
@@ -0,0 +1,159 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+type AutomationEvent = chrome.automation.AutomationEvent;
+type AutomationNode = chrome.automation.AutomationNode;
+import EventType = chrome.automation.EventType;
+
+interface EventHandlerOptions {
+  /**
+   * Whether to ignore events where the target is not the provided node.
+   */
+  exactMatch?: boolean;
+
+  /**
+   * True if the event should be processed before it has reached the target
+   * node, false if it should be processed after.
+   */
+  capture?: boolean;
+
+  /**
+   * True if the event listeners should automatically be removed when the
+   * callback is called once.
+   */
+  listenOnce?: boolean;
+
+  /**
+   * A predicate for what events will be processed.
+   */
+  predicate?: (event: AutomationEvent) => boolean;
+}
+
+/**
+ * This class wraps AutomationNode event listeners, adding some convenience
+ * functions.
+ */
+export class EventHandler {
+  private nodes_: AutomationNode[];
+  private types_: EventType[];
+  private callback_: ((event: AutomationEvent) => void)|null;
+  private capture_: boolean;
+  private exactMatch_: boolean;
+  private listenOnce_: boolean;
+  private listening_ = false;
+  private predicate_: (event: AutomationEvent) => boolean;
+  private handler_: (event: AutomationEvent) => void;
+
+  constructor(
+      nodes: AutomationNode|AutomationNode[], types: EventType|EventType[],
+      callback: (event: AutomationEvent) => void,
+      options: EventHandlerOptions = {}) {
+    this.nodes_ = nodes instanceof Array ? nodes : [nodes];
+    this.types_ = types instanceof Array ? types : [types];
+    this.callback_ = callback;
+    this.capture_ = options.capture || false;
+    this.exactMatch_ = options.exactMatch || false;
+    this.listenOnce_ = options.listenOnce || false;
+
+    /**
+     * Default is a function that always returns true.
+     */
+    this.predicate_ = options.predicate || (_e => true);
+
+    this.handler_ = event => this.handleEvent_(event);
+  }
+
+  /** Starts listening to events. */
+  start(): void {
+    if (this.listening_) {
+      return;
+    }
+
+    for (const node of this.nodes_) {
+      for (const type of this.types_) {
+        node.addEventListener(type, this.handler_, this.capture_);
+      }
+    }
+    this.listening_ = true;
+  }
+
+  /** Stops listening or handling future events. */
+  stop(): void {
+    for (const node of this.nodes_) {
+      for (const type of this.types_) {
+        node.removeEventListener(type, this.handler_, this.capture_);
+      }
+    }
+    this.listening_ = false;
+  }
+
+  /**
+   * @return Whether this EventHandler is currently listening for events.
+   */
+  listening(): boolean {
+    return this.listening_;
+  }
+
+  setCallback(callback: ((event: AutomationEvent) => void)|null): void {
+    this.callback_ = callback;
+  }
+
+  /**
+   * Changes what nodes are being listened to. Removes listeners from existing
+   *     nodes before adding listeners on new nodes.
+   */
+  setNodes(nodes: AutomationNode|AutomationNode[]): void {
+    const wasListening = this.listening_;
+    // TODO(b/318557827): Shouldn't this be: if (wasListening) this.stop()?
+    this.stop();
+    this.nodes_ = nodes instanceof Array ? nodes : [nodes];
+    if (wasListening) {
+      this.start();
+    }
+  }
+
+  /**
+   * Adds another node to the set of nodes being listened to.
+   */
+  addNode(node: AutomationNode): void {
+    this.nodes_.push(node);
+
+    if (this.listening_) {
+      for (const type of this.types_) {
+        node.addEventListener(type, this.handler_, this.capture_);
+      }
+    }
+  }
+
+  /**
+   * Removes a specific node from the set of nodes being listened to.
+   */
+  removeNode(node: AutomationNode): void {
+    this.nodes_ = this.nodes_.filter(n => n !== node);
+
+    if (this.listening_) {
+      for (const type of this.types_) {
+        node.removeEventListener(type, this.handler_, this.capture_);
+      }
+    }
+  }
+
+  private handleEvent_(event: AutomationEvent): void {
+    if (this.exactMatch_ && !this.nodes_.includes(event.target)) {
+      return;
+    }
+
+    if (!this.predicate_(event)) {
+      return;
+    }
+
+    if (this.listenOnce_) {
+      this.stop();
+    }
+
+    if (this.callback_) {
+      this.callback_(event);
+    }
+  }
+}
diff --git a/chrome/browser/resources/chromeos/cloud_upload/BUILD.gn b/chrome/browser/resources/chromeos/cloud_upload/BUILD.gn
index 9ac6ac40..0cfc988 100644
--- a/chrome/browser/resources/chromeos/cloud_upload/BUILD.gn
+++ b/chrome/browser/resources/chromeos/cloud_upload/BUILD.gn
@@ -62,7 +62,6 @@
     "//ash/webui/common/resources/cr_elements:build_ts",
     "//third_party/cros-components:cros_components_ts",
     "//ui/webui/resources/cr_components/color_change_listener:build_ts",
-    "//ui/webui/resources/cr_elements:build_ts",
     "//ui/webui/resources/js:build_ts",
     "//ui/webui/resources/mojo:build_ts",
   ]
diff --git a/chrome/browser/resources/chromeos/internet_detail_dialog/BUILD.gn b/chrome/browser/resources/chromeos/internet_detail_dialog/BUILD.gn
index eebb1b09..c1fb7de 100644
--- a/chrome/browser/resources/chromeos/internet_detail_dialog/BUILD.gn
+++ b/chrome/browser/resources/chromeos/internet_detail_dialog/BUILD.gn
@@ -15,6 +15,7 @@
 
   ts_definitions = [ "//tools/typescript/definitions/chrome_send.d.ts" ]
 
+  ts_composite = true
   ts_deps = [
     "//ash/webui/common/resources:build_ts",
     "//third_party/polymer/v3_0:library",
diff --git a/chrome/browser/safe_browsing/chrome_password_reuse_detection_manager_client.cc b/chrome/browser/safe_browsing/chrome_password_reuse_detection_manager_client.cc
index e04420a7..a9a2f91 100644
--- a/chrome/browser/safe_browsing/chrome_password_reuse_detection_manager_client.cc
+++ b/chrome/browser/safe_browsing/chrome_password_reuse_detection_manager_client.cc
@@ -267,9 +267,7 @@
 
   AddToWidgetInputEventObservers(page.GetMainDocument().GetRenderWidgetHost(),
                                  this);
-  if (base::FeatureList::IsEnabled(safe_browsing::kAntiPhishingTelemetry)) {
-    phishy_interaction_tracker_.HandlePageChanged();
-  }
+  phishy_interaction_tracker_.HandlePageChanged();
 }
 
 void ChromePasswordReuseDetectionManagerClient::RenderFrameCreated(
@@ -318,16 +316,12 @@
   }
 
   password_reuse_detection_manager_.OnPaste(std::move(text));
-  if (base::FeatureList::IsEnabled(safe_browsing::kAntiPhishingTelemetry)) {
-    phishy_interaction_tracker_.HandlePasteEvent();
-  }
+  phishy_interaction_tracker_.HandlePasteEvent();
 }
 
 void ChromePasswordReuseDetectionManagerClient::OnInputEvent(
     const blink::WebInputEvent& event) {
-  if (base::FeatureList::IsEnabled(safe_browsing::kAntiPhishingTelemetry)) {
-    phishy_interaction_tracker_.HandleInputEvent(event);
-  }
+  phishy_interaction_tracker_.HandleInputEvent(event);
 #if BUILDFLAG(IS_ANDROID)
   // On Android, key down events are triggered if a user types in through a
   // number bar on Android keyboard. If text is typed in through other parts of
diff --git a/chrome/browser/safe_browsing/phishy_interaction_tracker.cc b/chrome/browser/safe_browsing/phishy_interaction_tracker.cc
index 4bdc632..c815a93 100644
--- a/chrome/browser/safe_browsing/phishy_interaction_tracker.cc
+++ b/chrome/browser/safe_browsing/phishy_interaction_tracker.cc
@@ -12,7 +12,6 @@
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/safe_browsing/safe_browsing_service.h"
 #include "components/safe_browsing/content/browser/ui_manager.h"
-#include "components/safe_browsing/core/common/features.h"
 #include "content/public/browser/browser_thread.h"
 #include "ui/base/clipboard/clipboard.h"
 #include "ui/events/keycodes/keyboard_codes.h"
@@ -65,14 +64,11 @@
 PhishyInteractionTracker::PhishyInteractionTracker(
     content::WebContents* web_contents)
     : web_contents_(web_contents), inactivity_delay_(base::Minutes(5)) {
-  if (base::FeatureList::IsEnabled(safe_browsing::kAntiPhishingTelemetry)) {
-    ResetLoggingHelpers();
-  }
+  ResetLoggingHelpers();
 }
 
 PhishyInteractionTracker::~PhishyInteractionTracker() {
-  if (base::FeatureList::IsEnabled(safe_browsing::kAntiPhishingTelemetry) &&
-      is_phishy_ && !is_data_logged_) {
+  if (is_phishy_ && !is_data_logged_) {
     LogPageData();
     inactivity_timer_.Stop();
   }
diff --git a/chrome/browser/safe_browsing/phishy_interaction_tracker_unittest.cc b/chrome/browser/safe_browsing/phishy_interaction_tracker_unittest.cc
index 86c7e67..5aabeb8a 100644
--- a/chrome/browser/safe_browsing/phishy_interaction_tracker_unittest.cc
+++ b/chrome/browser/safe_browsing/phishy_interaction_tracker_unittest.cc
@@ -19,7 +19,6 @@
 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "components/safe_browsing/content/browser/unsafe_resource_util.h"
-#include "components/safe_browsing/core/common/features.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/mock_render_process_host.h"
@@ -89,7 +88,6 @@
         base::MakeRefCounted<safe_browsing::TestSafeBrowsingService>();
     browser_process_->SetSafeBrowsingService(sb_service_.get());
 
-    feature_list_.InitAndEnableFeature(safe_browsing::kAntiPhishingTelemetry);
     ui_manager_ = new StrictMock<MockSafeBrowsingUIManager>();
     phishy_interaction_tracker_ =
         base::WrapUnique(new PhishyInteractionTracker(web_contents()));
@@ -216,9 +214,6 @@
   scoped_refptr<safe_browsing::SafeBrowsingService> sb_service_;
   std::unique_ptr<PhishyInteractionTracker> phishy_interaction_tracker_;
   scoped_refptr<MockSafeBrowsingUIManager> ui_manager_;
-
- private:
-  base::test::ScopedFeatureList feature_list_;
 };
 
 TEST_F(PhishyInteractionTrackerTest, CheckHistogramCountsOnPhishyUserEvents) {
diff --git a/chrome/browser/safe_browsing/safe_browsing_blocking_page_test.cc b/chrome/browser/safe_browsing/safe_browsing_blocking_page_test.cc
index 59e0b3c..38be3e1 100644
--- a/chrome/browser/safe_browsing/safe_browsing_blocking_page_test.cc
+++ b/chrome/browser/safe_browsing/safe_browsing_blocking_page_test.cc
@@ -1054,27 +1054,6 @@
   net::EmbeddedTestServer https_server_;
 };
 
-class AntiPhishingTelemetryBrowserTest
-    : public SafeBrowsingBlockingPageBrowserTest {
- public:
-  AntiPhishingTelemetryBrowserTest() {
-    base::test::FeatureRefAndParams anti_phishing_telemetry_feature(
-        safe_browsing::kAntiPhishingTelemetry, {});
-    scoped_feature_list_.InitWithFeaturesAndParameters(
-        {anti_phishing_telemetry_feature}, {});
-  }
-  ~AntiPhishingTelemetryBrowserTest() override = default;
-
-  void SetUp() override { SafeBrowsingBlockingPageBrowserTest::SetUp(); }
-
-  content::WebContents* GetWebContents() {
-    return browser()->tab_strip_model()->GetActiveWebContents();
-  }
-
- private:
-  base::test::ScopedFeatureList scoped_feature_list_;
-};
-
 class SafeBrowsingHatsSurveyBrowserTest
     : public SafeBrowsingBlockingPageBrowserTest {
  public:
@@ -2416,6 +2395,9 @@
   EXPECT_TRUE(report.has_warning_shown_timestamp_msec());
 }
 
+class AntiPhishingTelemetryBrowserTest
+    : public SafeBrowsingBlockingPageBrowserTest {};
+
 INSTANTIATE_TEST_SUITE_P(
     AntiPhishingTelemetryBrowserTestWithThreatTypeAndIsolationSetting,
     AntiPhishingTelemetryBrowserTest,
diff --git a/chrome/browser/safe_browsing/safe_browsing_service.cc b/chrome/browser/safe_browsing/safe_browsing_service.cc
index 7e77958..2620082f 100644
--- a/chrome/browser/safe_browsing/safe_browsing_service.cc
+++ b/chrome/browser/safe_browsing/safe_browsing_service.cc
@@ -553,8 +553,7 @@
     const GURL& url,
     const GURL& page_url,
     const PhishySiteInteractionMap& phishy_interaction_data) {
-  if (!base::FeatureList::IsEnabled(kAntiPhishingTelemetry) || !profile ||
-      !IsExtendedReportingEnabled(*profile->GetPrefs())) {
+  if (!profile || !IsExtendedReportingEnabled(*profile->GetPrefs())) {
     return false;
   }
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
diff --git a/chrome/browser/safe_browsing/safe_browsing_service_unittest.cc b/chrome/browser/safe_browsing/safe_browsing_service_unittest.cc
index 3eb8b27..59a2a0d 100644
--- a/chrome/browser/safe_browsing/safe_browsing_service_unittest.cc
+++ b/chrome/browser/safe_browsing/safe_browsing_service_unittest.cc
@@ -16,7 +16,6 @@
 #include "components/download/public/common/mock_download_item.h"
 #include "components/safe_browsing/content/browser/safe_browsing_service_interface.h"
 #include "components/safe_browsing/core/browser/ping_manager.h"
-#include "components/safe_browsing/core/common/features.h"
 #include "components/safe_browsing/core/common/proto/csd.pb.h"
 #include "components/safe_browsing/core/common/safe_browsing_prefs.h"
 #include "content/public/browser/download_item_utils.h"
@@ -247,13 +246,8 @@
       /*show_download_in_folder=*/true));
 }
 
-class SafeBrowsingServiceTestWithAntiPhishingTelemetryEnabled
+class SafeBrowsingServiceAntiPhishingTelemetryTest
     : public SafeBrowsingServiceTest {
- public:
-  SafeBrowsingServiceTestWithAntiPhishingTelemetryEnabled() {
-    feature_list_.InitAndEnableFeature(safe_browsing::kAntiPhishingTelemetry);
-  }
-
  protected:
   PhishySiteInteractionMap SetUpPhishyInteractionMap(
       int expected_click_occurrences,
@@ -289,12 +283,9 @@
     }
     return new_map;
   }
-
- private:
-  base::test::ScopedFeatureList feature_list_;
 };
 
-TEST_F(SafeBrowsingServiceTestWithAntiPhishingTelemetryEnabled,
+TEST_F(SafeBrowsingServiceAntiPhishingTelemetryTest,
        SendPhishyInteractionsReport_Success) {
   const int kExpectedClickEventCount = 5;
   const int kExpectedKeyEventCount = 2;
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 8f024dd2..ed03f731 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -4736,6 +4736,8 @@
     sources += [
       "startup/web_app_info_recorder_utils.cc",
       "startup/web_app_info_recorder_utils.h",
+      "views/media_preview/active_devices_media_coordinator.cc",
+      "views/media_preview/active_devices_media_coordinator.h",
       "views/media_preview/camera_preview/camera_coordinator.cc",
       "views/media_preview/camera_preview/camera_coordinator.h",
       "views/media_preview/camera_preview/camera_mediator.cc",
diff --git a/chrome/browser/ui/ash/ambient/OWNERS b/chrome/browser/ui/ash/ambient/OWNERS
index 574ed11..67d3ebc4 100644
--- a/chrome/browser/ui/ash/ambient/OWNERS
+++ b/chrome/browser/ui/ash/ambient/OWNERS
@@ -1 +1 @@
-file://chromeos/ash/components/assistant/OWNERS
+file://ash/ambient/OWNERS
diff --git a/chrome/browser/ui/performance_controls/test_support/webui_interactive_test_mixin.h b/chrome/browser/ui/performance_controls/test_support/webui_interactive_test_mixin.h
new file mode 100644
index 0000000..089755d
--- /dev/null
+++ b/chrome/browser/ui/performance_controls/test_support/webui_interactive_test_mixin.h
@@ -0,0 +1,86 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_PERFORMANCE_CONTROLS_TEST_SUPPORT_WEBUI_INTERACTIVE_TEST_MIXIN_H_
+#define CHROME_BROWSER_UI_PERFORMANCE_CONTROLS_TEST_SUPPORT_WEBUI_INTERACTIVE_TEST_MIXIN_H_
+
+#include <type_traits>
+#include "base/test/bind.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_element_identifiers.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/interaction/interaction_test_util_browser.h"
+#include "chrome/test/interaction/interactive_browser_test.h"
+#include "chrome/test/interaction/webcontents_interaction_test_util.h"
+#include "ui/base/interaction/interactive_test.h"
+
+namespace {
+DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementRenders);
+DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kButtonWasClicked);
+DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kIronCollapseContentShows);
+}  // namespace
+
+// Template to be used as a mixin class for performance settings webui
+// interactive tests
+template <typename T>
+class WebUiInteractiveTestMixin : public T {
+ public:
+  template <class... Args>
+  explicit WebUiInteractiveTestMixin(Args&&... args)
+      : T(std::forward<Args>(args)...) {}
+
+  ~WebUiInteractiveTestMixin() override = default;
+
+  WebUiInteractiveTestMixin(const WebUiInteractiveTestMixin&) = delete;
+  WebUiInteractiveTestMixin& operator=(const WebUiInteractiveTestMixin&) =
+      delete;
+
+  auto WaitForElementToRender(
+      const ui::ElementIdentifier& contents_id,
+      const WebContentsInteractionTestUtil::DeepQuery& element) {
+    WebContentsInteractionTestUtil::StateChange element_renders;
+    element_renders.event = kElementRenders;
+    element_renders.where = element;
+    element_renders.test_function =
+        "(el) => { if (el !== null) { let rect = el.getBoundingClientRect(); "
+        "return rect.width > 0 && rect.height > 0; } return false; }";
+
+    return T::WaitForStateChange(contents_id, element_renders);
+  }
+
+  auto ClickElement(const ui::ElementIdentifier& contents_id,
+                    const WebContentsInteractionTestUtil::DeepQuery& element) {
+    return T::Steps(T::FlushEvents(),
+                    WaitForElementToRender(contents_id, element),
+                    T::MoveMouseTo(contents_id, element), T::ClickMouse());
+  }
+
+  auto WaitForButtonStateChange(
+      const ui::ElementIdentifier& contents_id,
+      const WebContentsInteractionTestUtil::DeepQuery& element,
+      bool is_checked) {
+    WebContentsInteractionTestUtil::StateChange toggle_selection_change;
+    toggle_selection_change.event = kButtonWasClicked;
+    toggle_selection_change.where = element;
+    toggle_selection_change.test_function = base::StrCat(
+        {"(el) => { return ", is_checked ? "" : "!", "el.checked; }"});
+
+    return T::WaitForStateChange(contents_id, toggle_selection_change);
+  }
+
+  auto WaitForIronListCollapseStateChange(
+      const ui::ElementIdentifier& contents_id,
+      const WebContentsInteractionTestUtil::DeepQuery& query) {
+    WebContentsInteractionTestUtil::StateChange iron_collapse_finish_animating;
+    iron_collapse_finish_animating.event = kIronCollapseContentShows;
+    iron_collapse_finish_animating.where = query;
+    iron_collapse_finish_animating.test_function =
+        "(el) => { return !el.transitioning; }";
+
+    return T::WaitForStateChange(contents_id, iron_collapse_finish_animating);
+  }
+};
+
+#endif  // CHROME_BROWSER_UI_PERFORMANCE_CONTROLS_TEST_SUPPORT_WEBUI_INTERACTIVE_TEST_MIXIN_H_
diff --git a/chrome/browser/ui/views/media_preview/active_devices_media_coordinator.cc b/chrome/browser/ui/views/media_preview/active_devices_media_coordinator.cc
new file mode 100644
index 0000000..3e2c293
--- /dev/null
+++ b/chrome/browser/ui/views/media_preview/active_devices_media_coordinator.cc
@@ -0,0 +1,139 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/views/media_preview/active_devices_media_coordinator.h"
+
+#include <string>
+#include <vector>
+
+#include "chrome/browser/ui/views/media_preview/media_view.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/media_device_id.h"
+#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
+#include "ui/views/view.h"
+
+namespace {
+
+using EligibleDevices = MediaCoordinator::EligibleDevices;
+
+bool IsWithinWebContents(content::GlobalRenderFrameHostId render_frame_host_id,
+                         content::WebContents* web_contents) {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  bool is_request_in_frame = false;
+  web_contents->GetPrimaryMainFrame()->ForEachRenderFrameHost(
+      [&is_request_in_frame,
+       render_frame_host_id](content::RenderFrameHost* render_frame_host) {
+        if (render_frame_host->GetGlobalId() == render_frame_host_id) {
+          is_request_in_frame = true;
+        }
+      });
+  return is_request_in_frame;
+}
+
+}  // namespace
+
+ActiveDevicesMediaCoordinator::ActiveDevicesMediaCoordinator(
+    content::WebContents* web_contents,
+    MediaCoordinator::ViewType view_type,
+    views::View* parent_view)
+    : web_contents_(web_contents),
+      view_type_(view_type),
+      parent_view_(parent_view),
+      stream_type_(view_type_ == MediaCoordinator::ViewType::kCameraOnly
+                       ? blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE
+                       : blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE) {
+  CHECK(parent_view_);
+  CHECK(web_contents_);
+  container_ = parent_view_->AddChildView(std::make_unique<MediaView>());
+  CHECK(container_);
+
+  MediaCaptureDevicesDispatcher::GetInstance()->AddObserver(this);
+  UpdateMediaCoordinatorList();
+}
+
+ActiveDevicesMediaCoordinator::~ActiveDevicesMediaCoordinator() {
+  MediaCaptureDevicesDispatcher::GetInstance()->RemoveObserver(this);
+}
+
+void ActiveDevicesMediaCoordinator::UpdateMediaCoordinatorList() {
+  web_contents_->GetMediaCaptureRawDeviceIdsOpened(
+      stream_type_,
+      base::BindOnce(
+          &ActiveDevicesMediaCoordinator::GotDeviceIdsOpenedForWebContents,
+          base::Unretained(this)));
+}
+
+void ActiveDevicesMediaCoordinator::GotDeviceIdsOpenedForWebContents(
+    std::vector<std::string> active_device_ids) {
+  if (active_device_ids.empty()) {
+    media_coordinators_.clear();
+    AddMediaCoordinatorForDevice(/*active_device_id=*/
+                                 std::nullopt);
+    return;
+  }
+
+  base::flat_set<std::string> active_device_id_set{active_device_ids};
+  for (const auto& key : GetMediaCoordinatorKeys()) {
+    if (!active_device_id_set.erase(key)) {
+      // remove the coordinator because it isn't active anymore.
+      media_coordinators_.erase(key);
+    }
+  }
+
+  for (const auto& active_device_id : active_device_id_set) {
+    AddMediaCoordinatorForDevice(active_device_id);
+  }
+}
+
+void ActiveDevicesMediaCoordinator::AddMediaCoordinatorForDevice(
+    const std::optional<std::string>& active_device_id) {
+  std::vector<std::string> active_device_id_vector;
+  if (active_device_id.has_value()) {
+    active_device_id_vector.push_back(active_device_id.value());
+  }
+  EligibleDevices eligible_devices;
+  if (view_type_ == MediaCoordinator::ViewType::kCameraOnly) {
+    eligible_devices.cameras = active_device_id_vector;
+  } else {
+    eligible_devices.mics = active_device_id_vector;
+  }
+
+  auto coordinator_key = active_device_id.value_or("changeable");
+  media_coordinators_.emplace(coordinator_key,
+                              std::make_unique<MediaCoordinator>(
+                                  view_type_, *container_,
+                                  /*index=*/std::nullopt,
+                                  /*is_subsection=*/true, eligible_devices));
+}
+
+void ActiveDevicesMediaCoordinator::OnRequestUpdate(
+    int render_process_id,
+    int render_frame_id,
+    blink::mojom::MediaStreamType stream_type,
+    const content::MediaRequestState state) {
+  if (stream_type != stream_type_) {
+    return;
+  }
+
+  if (!IsWithinWebContents(
+          content::GlobalRenderFrameHostId{render_process_id, render_frame_id},
+          web_contents_)) {
+    return;
+  }
+
+  if (state == content::MediaRequestState::MEDIA_REQUEST_STATE_DONE ||
+      state == content::MediaRequestState::MEDIA_REQUEST_STATE_CLOSING) {
+    UpdateMediaCoordinatorList();
+  }
+}
+
+std::vector<std::string>
+ActiveDevicesMediaCoordinator::GetMediaCoordinatorKeys() {
+  std::vector<std::string> keys;
+  keys.reserve(media_coordinators_.size());
+  for (const auto& [key, _] : media_coordinators_) {
+    keys.push_back(key);
+  }
+  return keys;
+}
diff --git a/chrome/browser/ui/views/media_preview/active_devices_media_coordinator.h b/chrome/browser/ui/views/media_preview/active_devices_media_coordinator.h
new file mode 100644
index 0000000..240cd90
--- /dev/null
+++ b/chrome/browser/ui/views/media_preview/active_devices_media_coordinator.h
@@ -0,0 +1,54 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_VIEWS_MEDIA_PREVIEW_ACTIVE_DEVICES_MEDIA_COORDINATOR_H_
+#define CHROME_BROWSER_UI_VIEWS_MEDIA_PREVIEW_ACTIVE_DEVICES_MEDIA_COORDINATOR_H_
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
+#include "chrome/browser/ui/views/media_preview/media_coordinator.h"
+#include "content/public/browser/web_contents.h"
+
+class ActiveDevicesMediaCoordinator
+    : public MediaCaptureDevicesDispatcher::Observer {
+ public:
+  ActiveDevicesMediaCoordinator(content::WebContents* web_contents,
+                                MediaCoordinator::ViewType view_type,
+                                views::View* parent_view);
+  ActiveDevicesMediaCoordinator(const ActiveDevicesMediaCoordinator&) = delete;
+  ActiveDevicesMediaCoordinator& operator=(
+      const ActiveDevicesMediaCoordinator&) = delete;
+  ~ActiveDevicesMediaCoordinator() override;
+
+ private:
+  void UpdateMediaCoordinatorList();
+
+  void GotDeviceIdsOpenedForWebContents(
+      std::vector<std::string> active_device_ids);
+
+  void AddMediaCoordinatorForDevice(
+      const std::optional<std::string>& active_device_id);
+
+  // MediaCaptureDevicesDispatcher::Observer impl.
+  void OnRequestUpdate(int render_process_id,
+                       int render_frame_id,
+                       blink::mojom::MediaStreamType stream_type,
+                       const content::MediaRequestState state) override;
+
+  std::vector<std::string> GetMediaCoordinatorKeys();
+
+  raw_ptr<content::WebContents> web_contents_;
+  MediaCoordinator::ViewType view_type_;
+  raw_ptr<views::View> parent_view_;
+  blink::mojom::MediaStreamType stream_type_;
+  raw_ptr<views::View> container_;
+  base::flat_map<std::string, std::unique_ptr<MediaCoordinator>>
+      media_coordinators_;
+};
+
+#endif  // CHROME_BROWSER_UI_VIEWS_MEDIA_PREVIEW_ACTIVE_DEVICES_MEDIA_COORDINATOR_H_
diff --git a/chrome/browser/ui/views/media_preview/camera_preview/camera_coordinator.cc b/chrome/browser/ui/views/media_preview/camera_preview/camera_coordinator.cc
index e1d44a21..901db5a 100644
--- a/chrome/browser/ui/views/media_preview/camera_preview/camera_coordinator.cc
+++ b/chrome/browser/ui/views/media_preview/camera_preview/camera_coordinator.cc
@@ -13,11 +13,14 @@
 #include "mojo/public/cpp/bindings/remote.h"
 #include "services/video_capture/public/mojom/video_source.mojom.h"
 
-CameraCoordinator::CameraCoordinator(views::View& parent_view,
-                                     bool needs_borders)
+CameraCoordinator::CameraCoordinator(
+    views::View& parent_view,
+    bool needs_borders,
+    const std::vector<std::string>& eligible_camera_ids)
     : camera_mediator_(
           base::BindRepeating(&CameraCoordinator::OnVideoSourceInfosReceived,
-                              base::Unretained(this))) {
+                              base::Unretained(this))),
+      eligible_camera_ids_(eligible_camera_ids) {
   auto* camera_view = parent_view.AddChildView(std::make_unique<MediaView>());
   camera_view_tracker_.SetView(camera_view);
   // Safe to use base::Unretained() because `this` owns / outlives
@@ -50,8 +53,12 @@
   }
 
   std::vector<VideoSourceInfo> relevant_device_infos;
-  relevant_device_infos.reserve(device_infos.size());
   for (const auto& device_info : device_infos) {
+    if (!eligible_camera_ids_.empty() &&
+        !eligible_camera_ids_.contains(device_info.descriptor.device_id)) {
+      continue;
+    }
+
     relevant_device_infos.emplace_back(device_info);
   }
 
diff --git a/chrome/browser/ui/views/media_preview/camera_preview/camera_coordinator.h b/chrome/browser/ui/views/media_preview/camera_preview/camera_coordinator.h
index c280ad6..de4f6464 100644
--- a/chrome/browser/ui/views/media_preview/camera_preview/camera_coordinator.h
+++ b/chrome/browser/ui/views/media_preview/camera_preview/camera_coordinator.h
@@ -19,7 +19,9 @@
 // Maintains the lifetime of its views.
 class CameraCoordinator {
  public:
-  CameraCoordinator(views::View& parent_view, bool needs_borders);
+  CameraCoordinator(views::View& parent_view,
+                    bool needs_borders,
+                    const std::vector<std::string>& eligible_camera_ids);
   CameraCoordinator(const CameraCoordinator&) = delete;
   CameraCoordinator& operator=(const CameraCoordinator&) = delete;
   ~CameraCoordinator();
@@ -44,6 +46,7 @@
   views::ViewTracker camera_view_tracker_;
   CameraSelectorComboboxModel combobox_model_;
   std::string active_device_id_;
+  base::flat_set<std::string> eligible_camera_ids_;
   std::optional<CameraViewController> camera_view_controller_;
   std::optional<VideoStreamCoordinator> video_stream_coordinator_;
 };
diff --git a/chrome/browser/ui/views/media_preview/camera_preview/camera_coordinator_unittest.cc b/chrome/browser/ui/views/media_preview/camera_preview/camera_coordinator_unittest.cc
index a282ef4..ab1f51b6 100644
--- a/chrome/browser/ui/views/media_preview/camera_preview/camera_coordinator_unittest.cc
+++ b/chrome/browser/ui/views/media_preview/camera_preview/camera_coordinator_unittest.cc
@@ -7,6 +7,7 @@
 #include <stddef.h>
 
 #include <memory>
+#include <optional>
 #include <string>
 
 #include "base/run_loop.h"
@@ -14,6 +15,7 @@
 #include "base/system/system_monitor.h"
 #include "base/test/gmock_callback_support.h"
 #include "base/test/mock_callback.h"
+#include "base/test/test_future.h"
 #include "chrome/browser/ui/views/frame/test_with_browser_view.h"
 #include "chrome/grit/generated_resources.h"
 #include "components/media_effects/test/fake_video_capture_service.h"
@@ -32,6 +34,22 @@
 constexpr char kDeviceId2[] = "device_id_2";
 constexpr char kDeviceName2[] = "device_name_2";
 
+MATCHER_P(HasItems, items, "") {
+  if (arg.GetItemCount() != items.size()) {
+    *result_listener << "item count is " << arg.GetItemCount();
+    return false;
+  }
+
+  for (size_t i = 0; i < items.size(); ++i) {
+    if (base::UTF8ToUTF16(items[i]) != arg.GetItemAt(i)) {
+      *result_listener << "item at index " << i << " is " << arg.GetItemAt(i);
+      return false;
+    }
+  }
+
+  return true;
+}
+
 }  // namespace
 
 class CameraCoordinatorTest : public TestWithBrowserView {
@@ -41,10 +59,9 @@
     content::OverrideVideoCaptureServiceForTesting(
         &fake_video_capture_service_);
     fake_video_capture_service_.SetOnGetVideoSourceCallback(
-        mock_video_source_callback_.Get());
-    parent_view_ = std::make_unique<views::View>();
-    coordinator_ = std::make_unique<CameraCoordinator>(*parent_view_,
-                                                       /*needs_borders=*/true);
+        on_get_video_source_future_.GetRepeatingCallback());
+    parent_view_.emplace();
+    InitializeCoordinator(/*eligible_camera_ids=*/{});
   }
 
   void TearDown() override {
@@ -55,6 +72,12 @@
     TestWithBrowserView::TearDown();
   }
 
+  void InitializeCoordinator(std::vector<std::string> eligible_camera_ids) {
+    coordinator_.emplace(*parent_view_,
+                         /*needs_borders=*/true,
+                         /*eligible_camera_ids=*/eligible_camera_ids);
+  }
+
   const CameraSelectorComboboxModel& GetComboboxModel() const {
     return coordinator_->GetComboboxModelForTest();
   }
@@ -69,82 +92,93 @@
         l10n_util::GetStringUTF16(IDS_MEDIA_PREVIEW_NO_CAMERAS_FOUND_COMBOBOX));
   }
 
+  bool AddFakeCamera(const media::VideoCaptureDeviceDescriptor& descriptor) {
+    replied_with_source_infos_future_.Clear();
+    fake_video_capture_service_.SetOnRepliedWithSourceInfosCallback(
+        replied_with_source_infos_future_.GetCallback());
+    fake_video_capture_service_.AddFakeCamera(descriptor);
+    return replied_with_source_infos_future_.WaitAndClear();
+  }
+
+  bool RemoveFakeCamera(const std::string& device_id) {
+    replied_with_source_infos_future_.Clear();
+    fake_video_capture_service_.SetOnRepliedWithSourceInfosCallback(
+        replied_with_source_infos_future_.GetCallback());
+    fake_video_capture_service_.RemoveFakeCamera(device_id);
+    return replied_with_source_infos_future_.WaitAndClear();
+  }
+
   base::SystemMonitor monitor_;
-  std::unique_ptr<views::View> parent_view_;
-  std::unique_ptr<CameraCoordinator> coordinator_;
+  std::optional<views::View> parent_view_;
+  std::optional<CameraCoordinator> coordinator_;
   media_effects::FakeVideoCaptureService fake_video_capture_service_;
-  base::MockCallback<
-      media_effects::FakeVideoSourceProvider::GetVideoSourceCallback>
-      mock_video_source_callback_;
+  base::test::TestFuture<void> replied_with_source_infos_future_;
+  base::test::TestFuture<
+      const std::string&,
+      mojo::PendingReceiver<video_capture::mojom::VideoSource>>
+      on_get_video_source_future_;
 };
 
 TEST_F(CameraCoordinatorTest, RelevantVideoCaptureDeviceInfoExtraction) {
   VerifyEmptyCombobox();
 
   // Add first camera, and connect to it.
-  // Camera connection is done automatically to the device at combobox's default
+  // camera connection is done automatically to the device at combobox's default
   // index (i.e. 0).
-  base::RunLoop run_loop;
-  EXPECT_CALL(mock_video_source_callback_, Run(kDeviceId, _))
-      .WillOnce(base::test::RunOnceClosure(run_loop.QuitClosure()));
-  fake_video_capture_service_.AddFakeCamera({kDeviceName, kDeviceId});
-  run_loop.Run();
-  EXPECT_EQ(GetComboboxModel().GetItemCount(), size_t(1));
-  EXPECT_EQ(GetComboboxModel().GetItemAt(/*index=*/0),
-            base::UTF8ToUTF16({kDeviceName}));
+  ASSERT_TRUE(AddFakeCamera({kDeviceName, kDeviceId}));
+  EXPECT_EQ(std::get<0>(on_get_video_source_future_.Take()), kDeviceId);
+  EXPECT_THAT(GetComboboxModel(), HasItems(std::vector{kDeviceName}));
 
   // Add second camera and connection to the first is not affected.
-  base::RunLoop run_loop2;
-  EXPECT_CALL(mock_video_source_callback_, Run).Times(0);
-  fake_video_capture_service_.SetOnRepliedWithSourceInfosCallback(
-      run_loop2.QuitClosure());
-  fake_video_capture_service_.AddFakeCamera({kDeviceName2, kDeviceId2});
-  run_loop2.Run();
-  EXPECT_EQ(GetComboboxModel().GetItemCount(), size_t(2));
-  EXPECT_EQ(GetComboboxModel().GetItemAt(/*index=*/0),
-            base::UTF8ToUTF16({kDeviceName}));
-  EXPECT_EQ(GetComboboxModel().GetItemAt(/*index=*/1),
-            base::UTF8ToUTF16({kDeviceName2}));
+  ASSERT_TRUE(AddFakeCamera({kDeviceName2, kDeviceId2}));
+  EXPECT_FALSE(on_get_video_source_future_.IsReady());
+  EXPECT_THAT(GetComboboxModel(),
+              HasItems(std::vector{kDeviceName, kDeviceName2}));
 
   // Remove first camera, and connect to the second one.
-  base::RunLoop run_loop3;
-  EXPECT_CALL(mock_video_source_callback_, Run(kDeviceId2, _))
-      .WillOnce(base::test::RunOnceClosure(run_loop3.QuitClosure()));
-  fake_video_capture_service_.RemoveFakeCamera(kDeviceId);
-  run_loop3.Run();
-  EXPECT_EQ(GetComboboxModel().GetItemCount(), size_t(1));
-  EXPECT_EQ(GetComboboxModel().GetItemAt(/*index=*/0),
-            base::UTF8ToUTF16({kDeviceName2}));
+  ASSERT_TRUE(RemoveFakeCamera(kDeviceId));
+  EXPECT_EQ(std::get<0>(on_get_video_source_future_.Take()), kDeviceId2);
+  EXPECT_THAT(GetComboboxModel(), HasItems(std::vector{kDeviceName2}));
 
   // Re-add first camera and connect to it.
-  base::RunLoop run_loop4;
-  EXPECT_CALL(mock_video_source_callback_, Run(kDeviceId, _))
-      .WillOnce(base::test::RunOnceClosure(run_loop4.QuitClosure()));
-  fake_video_capture_service_.AddFakeCamera({kDeviceName, kDeviceId});
-  run_loop4.Run();
-  EXPECT_EQ(GetComboboxModel().GetItemCount(), size_t(2));
-  EXPECT_EQ(GetComboboxModel().GetItemAt(/*index=*/0),
-            base::UTF8ToUTF16({kDeviceName}));
-  EXPECT_EQ(GetComboboxModel().GetItemAt(/*index=*/1),
-            base::UTF8ToUTF16({kDeviceName2}));
+  ASSERT_TRUE(AddFakeCamera({kDeviceName, kDeviceId}));
+  EXPECT_EQ(std::get<0>(on_get_video_source_future_.Take()), kDeviceId);
+  EXPECT_THAT(GetComboboxModel(),
+              HasItems(std::vector{kDeviceName, kDeviceName2}));
 
   // Remove second camera, and connection to the first is not affected.
-  base::RunLoop run_loop5;
-  EXPECT_CALL(mock_video_source_callback_, Run).Times(0);
-  fake_video_capture_service_.SetOnRepliedWithSourceInfosCallback(
-      run_loop5.QuitClosure());
-  fake_video_capture_service_.RemoveFakeCamera(kDeviceId2);
-  run_loop5.Run();
-  EXPECT_EQ(GetComboboxModel().GetItemCount(), size_t(1));
-  EXPECT_EQ(GetComboboxModel().GetItemAt(/*index=*/0),
-            base::UTF8ToUTF16({kDeviceName}));
+  ASSERT_TRUE(RemoveFakeCamera(kDeviceId2));
+  EXPECT_FALSE(on_get_video_source_future_.IsReady());
+  EXPECT_THAT(GetComboboxModel(), HasItems(std::vector{kDeviceName}));
 
   // Remove first camera.
-  base::RunLoop run_loop6;
-  fake_video_capture_service_.SetOnRepliedWithSourceInfosCallback(
-      run_loop6.QuitClosure());
-  fake_video_capture_service_.RemoveFakeCamera(kDeviceId);
-  run_loop6.Run();
+  ASSERT_TRUE(RemoveFakeCamera(kDeviceId));
+  VerifyEmptyCombobox();
+}
+
+TEST_F(CameraCoordinatorTest,
+       RelevantVideoCaptureDeviceInfoExtraction_ConstrainedToEligibleDevices) {
+  InitializeCoordinator({kDeviceId2});
+  VerifyEmptyCombobox();
+
+  // Add first camera. It won't be added to the combobox because it's not in the
+  // eligible list.
+  ASSERT_TRUE(AddFakeCamera({kDeviceName, kDeviceId}));
+  EXPECT_FALSE(on_get_video_source_future_.IsReady());
+  VerifyEmptyCombobox();
+
+  // Add second camera and connect to it since it's in the eligible list.
+  ASSERT_TRUE(AddFakeCamera({kDeviceName2, kDeviceId2}));
+  EXPECT_EQ(std::get<0>(on_get_video_source_future_.Take()), kDeviceId2);
+  EXPECT_THAT(GetComboboxModel(), HasItems(std::vector{kDeviceName2}));
+
+  // Remove first camera, nothing changes since it wasn't in the combobox.
+  ASSERT_TRUE(RemoveFakeCamera(kDeviceId));
+  EXPECT_FALSE(on_get_video_source_future_.IsReady());
+  EXPECT_THAT(GetComboboxModel(), HasItems(std::vector{kDeviceName2}));
+
+  // Remove second camera.
+  ASSERT_TRUE(RemoveFakeCamera(kDeviceId2));
   VerifyEmptyCombobox();
 }
 
@@ -152,56 +186,35 @@
   VerifyEmptyCombobox();
 
   // Add first camera, and connect to it.
-  base::RunLoop run_loop;
-  EXPECT_CALL(mock_video_source_callback_, Run(kDeviceId, _))
-      .WillOnce(base::test::RunOnceClosure(run_loop.QuitClosure()));
-  fake_video_capture_service_.AddFakeCamera({kDeviceName, kDeviceId});
-  run_loop.Run();
+  ASSERT_TRUE(AddFakeCamera({kDeviceName, kDeviceId}));
+  EXPECT_EQ(std::get<0>(on_get_video_source_future_.Take()), kDeviceId);
 
   // Add second camera and connection to the first is not affected.
-  base::RunLoop run_loop2;
-  EXPECT_CALL(mock_video_source_callback_, Run).Times(0);
-  fake_video_capture_service_.SetOnRepliedWithSourceInfosCallback(
-      run_loop2.QuitClosure());
-  fake_video_capture_service_.AddFakeCamera({kDeviceName2, kDeviceId2});
-  run_loop2.Run();
+  ASSERT_TRUE(AddFakeCamera({kDeviceName2, kDeviceId2}));
+  EXPECT_FALSE(on_get_video_source_future_.IsReady());
 
   //  Connect to the second camera.
-  base::RunLoop run_loop3;
-  EXPECT_CALL(mock_video_source_callback_, Run(kDeviceId2, _))
-      .WillOnce(base::test::RunOnceClosure(run_loop3.QuitClosure()));
   coordinator_->OnVideoSourceChanged(/*selected_index=*/1);
-  run_loop3.Run();
+  EXPECT_EQ(std::get<0>(on_get_video_source_future_.Take()), kDeviceId2);
 }
 
 TEST_F(CameraCoordinatorTest, TryConnectToSameDevice) {
   VerifyEmptyCombobox();
 
   // Add camera, and connect to it.
-  base::RunLoop run_loop;
-  EXPECT_CALL(mock_video_source_callback_, Run(kDeviceId, _))
-      .WillOnce(base::test::RunOnceClosure(run_loop.QuitClosure()));
-  fake_video_capture_service_.AddFakeCamera({kDeviceName, kDeviceId});
-  run_loop.Run();
+  ASSERT_TRUE(AddFakeCamera({kDeviceName, kDeviceId}));
+  EXPECT_EQ(std::get<0>(on_get_video_source_future_.Take()), kDeviceId);
 
   //  Try connect to the camera.
   // Nothing is expected because we are already connected.
-  EXPECT_CALL(mock_video_source_callback_, Run).Times(0);
   coordinator_->OnVideoSourceChanged(/*selected_index=*/0);
-  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(on_get_video_source_future_.IsReady());
 
   // Remove camera.
-  base::RunLoop run_loop2;
-  fake_video_capture_service_.SetOnRepliedWithSourceInfosCallback(
-      run_loop2.QuitClosure());
-  fake_video_capture_service_.RemoveFakeCamera(kDeviceId);
-  run_loop2.Run();
+  ASSERT_TRUE(RemoveFakeCamera(kDeviceId));
   VerifyEmptyCombobox();
 
   // Add camera, and connect to it again.
-  base::RunLoop run_loop3;
-  EXPECT_CALL(mock_video_source_callback_, Run(kDeviceId, _))
-      .WillOnce(base::test::RunOnceClosure(run_loop3.QuitClosure()));
-  fake_video_capture_service_.AddFakeCamera({kDeviceName, kDeviceId});
-  run_loop3.Run();
+  ASSERT_TRUE(AddFakeCamera({kDeviceName, kDeviceId}));
+  EXPECT_EQ(std::get<0>(on_get_video_source_future_.Take()), kDeviceId);
 }
diff --git a/chrome/browser/ui/views/media_preview/camera_preview/camera_view_controller.cc b/chrome/browser/ui/views/media_preview/camera_preview/camera_view_controller.cc
index 5277fdd..8c381ec4 100644
--- a/chrome/browser/ui/views/media_preview/camera_preview/camera_view_controller.cc
+++ b/chrome/browser/ui/views/media_preview/camera_preview/camera_view_controller.cc
@@ -34,7 +34,7 @@
 
 void CameraViewController::UpdateVideoSourceInfos(
     std::vector<VideoSourceInfo> video_source_infos) {
-  bool has_devices = !video_source_infos.empty();
+  auto video_source_info_count = video_source_infos.size();
   combobox_model_->UpdateDeviceList(std::move(video_source_infos));
-  base_controller_->AdjustComboboxEnabledState(has_devices);
+  base_controller_->OnDeviceListChanged(video_source_info_count);
 }
diff --git a/chrome/browser/ui/views/media_preview/media_coordinator.cc b/chrome/browser/ui/views/media_preview/media_coordinator.cc
index 8bb7deae..3f3092c 100644
--- a/chrome/browser/ui/views/media_preview/media_coordinator.cc
+++ b/chrome/browser/ui/views/media_preview/media_coordinator.cc
@@ -12,11 +12,21 @@
 #include "ui/views/border.h"
 #include "ui/views/view.h"
 
+MediaCoordinator::EligibleDevices::EligibleDevices() = default;
+MediaCoordinator::EligibleDevices::EligibleDevices(
+    std::vector<std::string> cameras,
+    std::vector<std::string> mics)
+    : cameras(cameras), mics(mics) {}
+MediaCoordinator::EligibleDevices::~EligibleDevices() = default;
+MediaCoordinator::EligibleDevices::EligibleDevices(const EligibleDevices&) =
+    default;
+
 MediaCoordinator::MediaCoordinator(ViewType view_type,
                                    views::View& parent_view,
                                    std::optional<size_t> index,
-                                   bool is_subsection) {
-  auto* media_view =
+                                   bool is_subsection,
+                                   EligibleDevices eligible_devices) {
+  media_view_ =
       parent_view.AddChildViewAt(std::make_unique<MediaView>(is_subsection),
                                  index.value_or(parent_view.children().size()));
 
@@ -27,19 +37,29 @@
     const int kBorderThickness =
         provider->GetDistanceMetric(views::DISTANCE_RELATED_CONTROL_VERTICAL);
 
-    media_view->SetBorder(views::CreateThemedRoundedRectBorder(
+    media_view_->SetBorder(views::CreateThemedRoundedRectBorder(
         kBorderThickness, kRoundedRadius, ui::kColorButtonBorder));
-    media_view->SetBackground(views::CreateThemedRoundedRectBackground(
+    media_view_->SetBackground(views::CreateThemedRoundedRectBackground(
         ui::kColorButtonBorder, kRoundedRadius));
   }
 
   if (view_type != ViewType::kMicOnly) {
-    camera_coordinator_.emplace(*media_view, /*needs_borders=*/!is_subsection);
+    camera_coordinator_.emplace(*media_view_, /*needs_borders=*/!is_subsection,
+                                eligible_devices.cameras);
   }
 
   if (view_type != ViewType::kCameraOnly) {
-    mic_coordinator_.emplace(*media_view, /*needs_borders=*/!is_subsection);
+    mic_coordinator_.emplace(*media_view_, /*needs_borders=*/!is_subsection,
+                             eligible_devices.mics);
   }
 }
 
-MediaCoordinator::~MediaCoordinator() = default;
+MediaCoordinator::~MediaCoordinator() {
+  // Reset child coordinators before removing view.
+  camera_coordinator_.reset();
+  mic_coordinator_.reset();
+  if (media_view_ && media_view_->parent()) {
+    media_view_->parent()->RemoveChildViewT(
+        std::exchange(media_view_, nullptr));
+  }
+}
diff --git a/chrome/browser/ui/views/media_preview/media_coordinator.h b/chrome/browser/ui/views/media_preview/media_coordinator.h
index b6af350f..6cdbd2f 100644
--- a/chrome/browser/ui/views/media_preview/media_coordinator.h
+++ b/chrome/browser/ui/views/media_preview/media_coordinator.h
@@ -8,6 +8,7 @@
 #include <stddef.h>
 
 #include <optional>
+#include <string>
 
 #include "chrome/browser/ui/views/media_preview/camera_preview/camera_coordinator.h"
 #include "chrome/browser/ui/views/media_preview/mic_preview/mic_coordinator.h"
@@ -21,15 +22,30 @@
  public:
   enum class ViewType { kBoth, kCameraOnly, kMicOnly };
 
+  // Specifies a selected device. Non-empty strings will cause the preview to
+  // display only that device and disable the combobox.
+  struct EligibleDevices {
+    EligibleDevices();
+    EligibleDevices(std::vector<std::string> cameras,
+                    std::vector<std::string> mics);
+    ~EligibleDevices();
+    EligibleDevices(const EligibleDevices&);
+
+    std::vector<std::string> cameras;
+    std::vector<std::string> mics;
+  };
+
   MediaCoordinator(ViewType view_type,
                    views::View& parent_view,
                    std::optional<size_t> index,
-                   bool is_subsection);
+                   bool is_subsection,
+                   EligibleDevices eligible_devices);
   MediaCoordinator(const MediaCoordinator&) = delete;
   MediaCoordinator& operator=(const MediaCoordinator&) = delete;
   ~MediaCoordinator();
 
  private:
+  raw_ptr<views::View> media_view_ = nullptr;
   std::optional<CameraCoordinator> camera_coordinator_;
   std::optional<MicCoordinator> mic_coordinator_;
 };
diff --git a/chrome/browser/ui/views/media_preview/media_view_controller_base.cc b/chrome/browser/ui/views/media_preview/media_view_controller_base.cc
index 5fcd45f..788d6b5 100644
--- a/chrome/browser/ui/views/media_preview/media_view_controller_base.cc
+++ b/chrome/browser/ui/views/media_preview/media_view_controller_base.cc
@@ -69,10 +69,11 @@
   device_selector_combobox_->SetCallback({});
 }
 
-void MediaViewControllerBase::AdjustComboboxEnabledState(bool has_devices) {
+void MediaViewControllerBase::OnDeviceListChanged(size_t device_count) {
+  bool has_devices = device_count > 0;
   live_feed_container_->SetVisible(has_devices);
   no_device_connected_label_->SetVisible(!has_devices);
-  device_selector_combobox_->SetEnabled(has_devices);
+  device_selector_combobox_->SetEnabled(device_count > 1);
   if (has_devices) {
     OnComboboxSelection();
   }
diff --git a/chrome/browser/ui/views/media_preview/media_view_controller_base.h b/chrome/browser/ui/views/media_preview/media_view_controller_base.h
index 3525e06..ae08661 100644
--- a/chrome/browser/ui/views/media_preview/media_view_controller_base.h
+++ b/chrome/browser/ui/views/media_preview/media_view_controller_base.h
@@ -46,9 +46,8 @@
   // Returns the immediate parent view of the live camera/mic feeds.
   MediaView& GetLiveFeedContainer() { return live_feed_container_.get(); }
 
-  // Enables the combobox if there are connected devices (e.g.`has_devices` is
-  // true).
-  void AdjustComboboxEnabledState(bool has_devices);
+  // Enables the combobox if `device_count` > 1.
+  void OnDeviceListChanged(size_t device_count);
 
  private:
   friend class MediaViewControllerBaseTest;
diff --git a/chrome/browser/ui/views/media_preview/media_view_controller_base_unittest.cc b/chrome/browser/ui/views/media_preview/media_view_controller_base_unittest.cc
index 47f5839..692ca287c 100644
--- a/chrome/browser/ui/views/media_preview/media_view_controller_base_unittest.cc
+++ b/chrome/browser/ui/views/media_preview/media_view_controller_base_unittest.cc
@@ -16,6 +16,9 @@
 #include "ui/views/controls/combobox/combobox.h"
 #include "ui/views/controls/label.h"
 
+using testing::_;
+using testing::Eq;
+
 class MediaViewControllerBaseTest : public TestWithBrowserView {
  protected:
   void SetUp() override {
@@ -45,14 +48,34 @@
   std::unique_ptr<MediaViewControllerBase> controller_;
 };
 
-TEST_F(MediaViewControllerBaseTest, UpdateComboboxEnabledStateTest) {
+TEST_F(MediaViewControllerBaseTest, OnDeviceListChanged_NoDevices) {
   EXPECT_TRUE(IsNoDeviceLabelVisible());
   EXPECT_FALSE(IsComboboxEnabled());
 
-  EXPECT_CALL(source_change_callback_, Run(testing::_))
-      .WillOnce(
-          [](std::optional<size_t> index) { EXPECT_EQ(std::nullopt, index); });
-  controller_->AdjustComboboxEnabledState(/*has_devices=*/true);
+  EXPECT_CALL(source_change_callback_, Run(_)).Times(0);
+  controller_->OnDeviceListChanged(/*device_count=*/0);
+
+  EXPECT_TRUE(IsNoDeviceLabelVisible());
+  EXPECT_FALSE(IsComboboxEnabled());
+}
+
+TEST_F(MediaViewControllerBaseTest, OnDeviceListChanged_OneDevice) {
+  EXPECT_TRUE(IsNoDeviceLabelVisible());
+  EXPECT_FALSE(IsComboboxEnabled());
+
+  EXPECT_CALL(source_change_callback_, Run(Eq(std::nullopt)));
+  controller_->OnDeviceListChanged(/*device_count=*/1);
+
+  EXPECT_FALSE(IsNoDeviceLabelVisible());
+  EXPECT_FALSE(IsComboboxEnabled());
+}
+
+TEST_F(MediaViewControllerBaseTest, OnDeviceListChanged_MultipleDevices) {
+  EXPECT_TRUE(IsNoDeviceLabelVisible());
+  EXPECT_FALSE(IsComboboxEnabled());
+
+  EXPECT_CALL(source_change_callback_, Run(Eq(std::nullopt)));
+  controller_->OnDeviceListChanged(/*device_count=*/2);
 
   EXPECT_FALSE(IsNoDeviceLabelVisible());
   EXPECT_TRUE(IsComboboxEnabled());
diff --git a/chrome/browser/ui/views/media_preview/mic_preview/mic_coordinator.cc b/chrome/browser/ui/views/media_preview/mic_preview/mic_coordinator.cc
index 95024aa..d2e78ade 100644
--- a/chrome/browser/ui/views/media_preview/mic_preview/mic_coordinator.cc
+++ b/chrome/browser/ui/views/media_preview/mic_preview/mic_coordinator.cc
@@ -30,10 +30,13 @@
 
 }  // namespace
 
-MicCoordinator::MicCoordinator(views::View& parent_view, bool needs_borders)
+MicCoordinator::MicCoordinator(views::View& parent_view,
+                               bool needs_borders,
+                               const std::vector<std::string>& eligible_mic_ids)
     : mic_mediator_(
           base::BindRepeating(&MicCoordinator::OnAudioSourceInfosReceived,
-                              base::Unretained(this))) {
+                              base::Unretained(this))),
+      eligible_mic_ids_(eligible_mic_ids) {
   auto* mic_view = parent_view.AddChildView(std::make_unique<MediaView>());
   mic_view_tracker_.SetView(mic_view);
   // Safe to use base::Unretained() because `this` owns / outlives
@@ -72,6 +75,10 @@
         media::AudioDeviceDescription::kDefaultDeviceId) {
       continue;
     }
+    if (!eligible_mic_ids_.empty() &&
+        !eligible_mic_ids_.contains(device_info.unique_id)) {
+      continue;
+    }
     bool is_default =
         system_default_device_name &&
         device_info.device_name == system_default_device_name.value();
diff --git a/chrome/browser/ui/views/media_preview/mic_preview/mic_coordinator.h b/chrome/browser/ui/views/media_preview/mic_preview/mic_coordinator.h
index 3100201..7620b21 100644
--- a/chrome/browser/ui/views/media_preview/mic_preview/mic_coordinator.h
+++ b/chrome/browser/ui/views/media_preview/mic_preview/mic_coordinator.h
@@ -24,7 +24,9 @@
 // Maintains the lifetime of its views.
 class MicCoordinator {
  public:
-  MicCoordinator(views::View& parent_view, bool needs_borders);
+  MicCoordinator(views::View& parent_view,
+                 bool needs_borders,
+                 const std::vector<std::string>& eligible_mic_ids);
   MicCoordinator(const MicCoordinator&) = delete;
   MicCoordinator& operator=(const MicCoordinator&) = delete;
   ~MicCoordinator();
@@ -53,6 +55,7 @@
   views::ViewTracker mic_view_tracker_;
   MicSelectorComboboxModel combobox_model_;
   std::string active_device_id_;
+  base::flat_set<std::string> eligible_mic_ids_;
   std::optional<MicViewController> mic_view_controller_;
   std::optional<AudioStreamCoordinator> audio_stream_coordinator_;
 };
diff --git a/chrome/browser/ui/views/media_preview/mic_preview/mic_coordinator_unittest.cc b/chrome/browser/ui/views/media_preview/mic_preview/mic_coordinator_unittest.cc
index aa6b374..08d19664 100644
--- a/chrome/browser/ui/views/media_preview/mic_preview/mic_coordinator_unittest.cc
+++ b/chrome/browser/ui/views/media_preview/mic_preview/mic_coordinator_unittest.cc
@@ -65,9 +65,8 @@
     fake_audio_service_.SetOnRepliedWithInputDeviceDescriptionsCallback(
         replied_with_device_descriptions_future_.GetCallback());
 
-    parent_view_ = std::make_unique<views::View>();
-    coordinator_ = std::make_unique<MicCoordinator>(*parent_view_,
-                                                    /*needs_borders=*/true);
+    parent_view_.emplace();
+    InitializeCoordinator({});
     ASSERT_TRUE(replied_with_device_descriptions_future_.WaitAndClear());
   }
 
@@ -76,6 +75,12 @@
     TestWithBrowserView::TearDown();
   }
 
+  void InitializeCoordinator(std::vector<std::string> eligible_mic_ids) {
+    coordinator_.emplace(*parent_view_,
+                         /*needs_borders=*/true,
+                         /*eligible_mic_ids=*/eligible_mic_ids);
+  }
+
   const MicSelectorComboboxModel& GetComboboxModel() const {
     return coordinator_->GetComboboxModelForTest();
   }
@@ -90,6 +95,22 @@
         l10n_util::GetStringUTF16(IDS_MEDIA_PREVIEW_NO_MICS_FOUND_COMBOBOX));
   }
 
+  bool AddFakeInputDevice(const media::AudioDeviceDescription& descriptor) {
+    replied_with_device_descriptions_future_.Clear();
+    fake_audio_service_.SetOnRepliedWithInputDeviceDescriptionsCallback(
+        replied_with_device_descriptions_future_.GetCallback());
+    fake_audio_service_.AddFakeInputDevice(descriptor);
+    return replied_with_device_descriptions_future_.WaitAndClear();
+  }
+
+  bool RemoveFakeInputDevice(const std::string& device_id) {
+    replied_with_device_descriptions_future_.Clear();
+    fake_audio_service_.SetOnRepliedWithInputDeviceDescriptionsCallback(
+        replied_with_device_descriptions_future_.GetCallback());
+    fake_audio_service_.RemoveFakeInputDevice(device_id);
+    return replied_with_device_descriptions_future_.WaitAndClear();
+  }
+
   base::SystemMonitor monitor_;
   media_effects::FakeAudioService fake_audio_service_;
   std::optional<base::AutoReset<audio::mojom::AudioService*>>
@@ -99,8 +120,8 @@
   base::test::TestFuture<void> on_bind_stream_factory_future_;
   base::test::TestFuture<void> replied_with_device_descriptions_future_;
 
-  std::unique_ptr<views::View> parent_view_;
-  std::unique_ptr<MicCoordinator> coordinator_;
+  std::optional<views::View> parent_view_;
+  std::optional<MicCoordinator> coordinator_;
 };
 
 TEST_F(MicCoordinatorTest, RelevantAudioInputDeviceInfoExtraction) {
@@ -109,46 +130,64 @@
   // Add first mic, and connect to it.
   // Mic connection is done automatically to the device at combobox's default
   // index (i.e. 0).
-  fake_audio_service_.AddFakeInputDevice({kDeviceName, kDeviceId, kGroupId});
+  ASSERT_TRUE(AddFakeInputDevice({kDeviceName, kDeviceId, kGroupId}));
   ASSERT_TRUE(on_bind_stream_factory_future_.WaitAndClear());
   EXPECT_EQ(on_input_stream_id_future_.Take(), kDeviceId);
   EXPECT_THAT(GetComboboxModel(), HasItems(std::vector{kDeviceName}));
 
   // Add second mic and connection to the first is not affected.
-  fake_audio_service_.SetOnRepliedWithInputDeviceDescriptionsCallback(
-      replied_with_device_descriptions_future_.GetCallback());
-  fake_audio_service_.AddFakeInputDevice({kDeviceName2, kDeviceId2, kGroupId2});
-  ASSERT_TRUE(replied_with_device_descriptions_future_.WaitAndClear());
+  ASSERT_TRUE(AddFakeInputDevice({kDeviceName2, kDeviceId2, kGroupId2}));
   EXPECT_FALSE(on_input_stream_id_future_.IsReady());
   EXPECT_THAT(GetComboboxModel(),
               HasItems(std::vector{kDeviceName, kDeviceName2}));
 
   // Remove first mic, and connect to the second one.
-  fake_audio_service_.RemoveFakeInputDevice(kDeviceId);
+  ASSERT_TRUE(RemoveFakeInputDevice(kDeviceId));
   ASSERT_TRUE(on_bind_stream_factory_future_.WaitAndClear());
   EXPECT_EQ(on_input_stream_id_future_.Take(), kDeviceId2);
   EXPECT_THAT(GetComboboxModel(), HasItems(std::vector{kDeviceName2}));
 
   // Re-add first mic and connect to it.
-  fake_audio_service_.AddFakeInputDevice({kDeviceName, kDeviceId, kGroupId});
+  ASSERT_TRUE(AddFakeInputDevice({kDeviceName, kDeviceId, kGroupId}));
   ASSERT_TRUE(on_bind_stream_factory_future_.WaitAndClear());
   EXPECT_EQ(on_input_stream_id_future_.Take(), kDeviceId);
   EXPECT_THAT(GetComboboxModel(),
               HasItems(std::vector{kDeviceName, kDeviceName2}));
 
   // Remove second mic, and connection to the first is not affected.
-  fake_audio_service_.SetOnRepliedWithInputDeviceDescriptionsCallback(
-      replied_with_device_descriptions_future_.GetCallback());
-  fake_audio_service_.RemoveFakeInputDevice(kDeviceId2);
-  ASSERT_TRUE(replied_with_device_descriptions_future_.WaitAndClear());
+  ASSERT_TRUE(RemoveFakeInputDevice(kDeviceId2));
   EXPECT_FALSE(on_input_stream_id_future_.IsReady());
   EXPECT_THAT(GetComboboxModel(), HasItems(std::vector{kDeviceName}));
 
   // Remove first mic.
-  fake_audio_service_.SetOnRepliedWithInputDeviceDescriptionsCallback(
-      replied_with_device_descriptions_future_.GetCallback());
-  fake_audio_service_.RemoveFakeInputDevice(kDeviceId);
-  ASSERT_TRUE(replied_with_device_descriptions_future_.WaitAndClear());
+  ASSERT_TRUE(RemoveFakeInputDevice(kDeviceId));
+  VerifyEmptyCombobox();
+}
+
+TEST_F(MicCoordinatorTest,
+       RelevantAudioInputDeviceInfoExtraction_ConstrainedToEligibleDevices) {
+  InitializeCoordinator({kDeviceId2});
+  VerifyEmptyCombobox();
+
+  // Add first mic. It won't be added to the combobox because it's not in the
+  // eligible list.
+  ASSERT_TRUE(AddFakeInputDevice({kDeviceName, kDeviceId, kGroupId}));
+  EXPECT_FALSE(on_input_stream_id_future_.IsReady());
+  VerifyEmptyCombobox();
+
+  // Add second mic and connect to it since it's in the eligible list.
+  ASSERT_TRUE(AddFakeInputDevice({kDeviceName2, kDeviceId2, kGroupId2}));
+  ASSERT_TRUE(on_bind_stream_factory_future_.WaitAndClear());
+  EXPECT_EQ(on_input_stream_id_future_.Take(), kDeviceId2);
+  EXPECT_THAT(GetComboboxModel(), HasItems(std::vector{kDeviceName2}));
+
+  // Remove first mic, nothing changes since it wasn't in the combobox.
+  ASSERT_TRUE(RemoveFakeInputDevice(kDeviceId));
+  EXPECT_FALSE(on_input_stream_id_future_.IsReady());
+  EXPECT_THAT(GetComboboxModel(), HasItems(std::vector{kDeviceName2}));
+
+  // Remove second mic.
+  ASSERT_TRUE(RemoveFakeInputDevice(kDeviceId2));
   VerifyEmptyCombobox();
 }
 
@@ -156,15 +195,12 @@
   VerifyEmptyCombobox();
 
   // Add first mic, and connect to it.
-  fake_audio_service_.AddFakeInputDevice({kDeviceName, kDeviceId, kGroupId});
+  ASSERT_TRUE(AddFakeInputDevice({kDeviceName, kDeviceId, kGroupId}));
   ASSERT_TRUE(on_bind_stream_factory_future_.WaitAndClear());
   EXPECT_EQ(on_input_stream_id_future_.Take(), kDeviceId);
 
   // Add second mic and connection to the first is not affected.
-  fake_audio_service_.SetOnRepliedWithInputDeviceDescriptionsCallback(
-      replied_with_device_descriptions_future_.GetCallback());
-  fake_audio_service_.AddFakeInputDevice({kDeviceName2, kDeviceId2, kGroupId2});
-  ASSERT_TRUE(replied_with_device_descriptions_future_.WaitAndClear());
+  ASSERT_TRUE(AddFakeInputDevice({kDeviceName2, kDeviceId2, kGroupId2}));
   EXPECT_FALSE(on_input_stream_id_future_.IsReady());
 
   //  Connect to the second mic.
@@ -177,7 +213,7 @@
   VerifyEmptyCombobox();
 
   // Add mic, and connect to it.
-  fake_audio_service_.AddFakeInputDevice({kDeviceName, kDeviceId, kGroupId});
+  ASSERT_TRUE(AddFakeInputDevice({kDeviceName, kDeviceId, kGroupId}));
   ASSERT_TRUE(on_bind_stream_factory_future_.WaitAndClear());
   EXPECT_EQ(on_input_stream_id_future_.Take(), kDeviceId);
 
@@ -188,14 +224,11 @@
   EXPECT_FALSE(on_input_stream_id_future_.IsReady());
 
   // Remove mic.
-  fake_audio_service_.SetOnRepliedWithInputDeviceDescriptionsCallback(
-      replied_with_device_descriptions_future_.GetCallback());
-  fake_audio_service_.RemoveFakeInputDevice(kDeviceId);
-  ASSERT_TRUE(replied_with_device_descriptions_future_.WaitAndClear());
+  ASSERT_TRUE(RemoveFakeInputDevice(kDeviceId));
   VerifyEmptyCombobox();
 
   // Add mic, and connect to it again.
-  fake_audio_service_.AddFakeInputDevice({kDeviceName, kDeviceId, kGroupId});
+  ASSERT_TRUE(AddFakeInputDevice({kDeviceName, kDeviceId, kGroupId}));
   ASSERT_TRUE(on_bind_stream_factory_future_.WaitAndClear());
   EXPECT_EQ(on_input_stream_id_future_.Take(), kDeviceId);
 }
@@ -204,7 +237,7 @@
   VerifyEmptyCombobox();
 
   // Add mic, and connect to it.
-  fake_audio_service_.AddFakeInputDevice({kDeviceName, kDeviceId, kGroupId});
+  ASSERT_TRUE(AddFakeInputDevice({kDeviceName, kDeviceId, kGroupId}));
   ASSERT_TRUE(on_bind_stream_factory_future_.WaitAndClear());
   EXPECT_EQ(on_input_stream_id_future_.Take(), kDeviceId);
   EXPECT_THAT(GetComboboxModel(), HasItems(std::vector{kDeviceName}));
@@ -213,11 +246,9 @@
 
   // Add the same mic again with the default id.
   // One mic is expected to exist in the model with secondary text as default.
-  fake_audio_service_.SetOnRepliedWithInputDeviceDescriptionsCallback(
-      replied_with_device_descriptions_future_.GetCallback());
-  fake_audio_service_.AddFakeInputDevice(
-      {kDeviceName, media::AudioDeviceDescription::kDefaultDeviceId, kGroupId});
-  ASSERT_TRUE(replied_with_device_descriptions_future_.WaitAndClear());
+  ASSERT_TRUE(AddFakeInputDevice(
+      {kDeviceName, media::AudioDeviceDescription::kDefaultDeviceId,
+       kGroupId}));
   EXPECT_FALSE(on_input_stream_id_future_.IsReady());
   EXPECT_THAT(GetComboboxModel(), HasItems(std::vector{kDeviceName}));
   EXPECT_EQ(GetComboboxModel().GetDropDownSecondaryTextAt(/*index=*/0),
diff --git a/chrome/browser/ui/views/media_preview/mic_preview/mic_view_controller.cc b/chrome/browser/ui/views/media_preview/mic_preview/mic_view_controller.cc
index baadda80..fbd9da0 100644
--- a/chrome/browser/ui/views/media_preview/mic_preview/mic_view_controller.cc
+++ b/chrome/browser/ui/views/media_preview/mic_preview/mic_view_controller.cc
@@ -65,7 +65,7 @@
 
 void MicViewController::UpdateAudioSourceInfos(
     std::vector<AudioSourceInfo> audio_source_infos) {
-  bool has_devices = !audio_source_infos.empty();
+  auto audio_source_info_count = audio_source_infos.size();
   combobox_model_->UpdateDeviceList(std::move(audio_source_infos));
-  base_controller_->AdjustComboboxEnabledState(has_devices);
+  base_controller_->OnDeviceListChanged(audio_source_info_count);
 }
diff --git a/chrome/browser/ui/views/page_info/page_info_bubble_view.cc b/chrome/browser/ui/views/page_info/page_info_bubble_view.cc
index 467a74b..0af24f2 100644
--- a/chrome/browser/ui/views/page_info/page_info_bubble_view.cc
+++ b/chrome/browser/ui/views/page_info/page_info_bubble_view.cc
@@ -229,7 +229,7 @@
   presenter_->RecordPageInfoAction(
       PageInfo::PageInfoAction::PAGE_INFO_PERMISSION_DIALOG_OPENED);
   std::unique_ptr<views::View> permissions_page_view =
-      view_factory_->CreatePermissionPageView(type);
+      view_factory_->CreatePermissionPageView(type, web_contents());
   permissions_page_view->SetID(
       PageInfoViewFactory::VIEW_ID_PAGE_INFO_CURRENT_VIEW);
   page_container_->SwitchToPage(std::move(permissions_page_view));
diff --git a/chrome/browser/ui/views/page_info/page_info_permission_content_view.cc b/chrome/browser/ui/views/page_info/page_info_permission_content_view.cc
index 937aa5b..7923e9a7 100644
--- a/chrome/browser/ui/views/page_info/page_info_permission_content_view.cc
+++ b/chrome/browser/ui/views/page_info/page_info_permission_content_view.cc
@@ -28,8 +28,12 @@
 PageInfoPermissionContentView::PageInfoPermissionContentView(
     PageInfo* presenter,
     ChromePageInfoUiDelegate* ui_delegate,
-    ContentSettingsType type)
-    : presenter_(presenter), type_(type), ui_delegate_(ui_delegate) {
+    ContentSettingsType type,
+    content::WebContents* web_contents)
+    : presenter_(presenter),
+      type_(type),
+      ui_delegate_(ui_delegate),
+      web_contents_(web_contents) {
   ChromeLayoutProvider* layout_provider = ChromeLayoutProvider::Get();
 
   // Use the same insets as buttons and permission rows in the main page for
@@ -209,7 +213,7 @@
   auto view_type = type_ == ContentSettingsType::MEDIASTREAM_CAMERA
                        ? MediaCoordinator::ViewType::kCameraOnly
                        : MediaCoordinator::ViewType::kMicOnly;
-  media_preview_coordinator_.emplace(view_type, *this, /*index=*/std::nullopt,
-                                     /*is_subsection=*/true);
+  active_devices_media_preview_coordinator_.emplace(web_contents_, view_type,
+                                                    /*parent_view=*/this);
 #endif
 }
diff --git a/chrome/browser/ui/views/page_info/page_info_permission_content_view.h b/chrome/browser/ui/views/page_info/page_info_permission_content_view.h
index f3f39be..8b1baa9 100644
--- a/chrome/browser/ui/views/page_info/page_info_permission_content_view.h
+++ b/chrome/browser/ui/views/page_info/page_info_permission_content_view.h
@@ -10,7 +10,7 @@
 #include "ui/views/view.h"
 
 #if !BUILDFLAG(IS_CHROMEOS) && !BUILDFLAG(IS_FUCHSIA)
-#include "chrome/browser/ui/views/media_preview/media_coordinator.h"
+#include "chrome/browser/ui/views/media_preview/active_devices_media_coordinator.h"
 #endif
 
 class ChromePageInfoUiDelegate;
@@ -38,7 +38,8 @@
  public:
   PageInfoPermissionContentView(PageInfo* presenter,
                                 ChromePageInfoUiDelegate* ui_delegate,
-                                ContentSettingsType type);
+                                ContentSettingsType type,
+                                content::WebContents* web_contents);
   ~PageInfoPermissionContentView() override;
 
   // PageInfoUI implementations.
@@ -60,6 +61,7 @@
   ContentSettingsType type_;
   raw_ptr<ChromePageInfoUiDelegate> ui_delegate_ = nullptr;
   PageInfo::PermissionInfo permission_;
+  raw_ptr<content::WebContents> web_contents_ = nullptr;
 
   raw_ptr<NonAccessibleImageView> icon_ = nullptr;
   raw_ptr<views::Label> title_ = nullptr;
@@ -68,7 +70,8 @@
   raw_ptr<views::Checkbox> remember_setting_ = nullptr;
 
 #if !BUILDFLAG(IS_CHROMEOS) && !BUILDFLAG(IS_FUCHSIA)
-  std::optional<MediaCoordinator> media_preview_coordinator_;
+  std::optional<ActiveDevicesMediaCoordinator>
+      active_devices_media_preview_coordinator_;
 #endif
 };
 
diff --git a/chrome/browser/ui/views/page_info/page_info_view_factory.cc b/chrome/browser/ui/views/page_info/page_info_view_factory.cc
index 78e5e46..05bc896 100644
--- a/chrome/browser/ui/views/page_info/page_info_view_factory.cc
+++ b/chrome/browser/ui/views/page_info/page_info_view_factory.cc
@@ -144,12 +144,13 @@
 }
 
 std::unique_ptr<views::View> PageInfoViewFactory::CreatePermissionPageView(
-    ContentSettingsType type) {
+    ContentSettingsType type,
+    content::WebContents* web_contents) {
   return std::make_unique<PageInfoSubpageView>(
       CreateSubpageHeader(PageInfoUI::PermissionTypeToUIString(type),
                           presenter_->GetSubjectNameForDisplay()),
       std::make_unique<PageInfoPermissionContentView>(presenter_, ui_delegate_,
-                                                      type));
+                                                      type, web_contents));
 }
 
 std::unique_ptr<views::View>
diff --git a/chrome/browser/ui/views/page_info/page_info_view_factory.h b/chrome/browser/ui/views/page_info/page_info_view_factory.h
index 89a389f..4aeb222 100644
--- a/chrome/browser/ui/views/page_info/page_info_view_factory.h
+++ b/chrome/browser/ui/views/page_info/page_info_view_factory.h
@@ -160,7 +160,8 @@
       base::OnceClosure initialized_callback);
   [[nodiscard]] std::unique_ptr<views::View> CreateSecurityPageView();
   [[nodiscard]] std::unique_ptr<views::View> CreatePermissionPageView(
-      ContentSettingsType type);
+      ContentSettingsType type,
+      content::WebContents* web_contents);
   [[nodiscard]] std::unique_ptr<views::View> CreateAdPersonalizationPageView();
   [[nodiscard]] std::unique_ptr<views::View> CreateCookiesPageView();
 
diff --git a/chrome/browser/ui/views/permissions/permission_prompt_bubble_one_origin_view.cc b/chrome/browser/ui/views/permissions/permission_prompt_bubble_one_origin_view.cc
index 7d5b9ea3..96a9c3324 100644
--- a/chrome/browser/ui/views/permissions/permission_prompt_bubble_one_origin_view.cc
+++ b/chrome/browser/ui/views/permissions/permission_prompt_bubble_one_origin_view.cc
@@ -252,6 +252,7 @@
   }
 
   media_preview_coordinator_.emplace(view_type.value(), *this, index,
-                                     /*is_subsection=*/false);
+                                     /*is_subsection=*/false,
+                                     MediaCoordinator::EligibleDevices{});
 #endif
 }
diff --git a/chrome/browser/ui/webui/ash/login/core_oobe_handler.cc b/chrome/browser/ui/webui/ash/login/core_oobe_handler.cc
index d3bfac49..3afa0d1d 100644
--- a/chrome/browser/ui/webui/ash/login/core_oobe_handler.cc
+++ b/chrome/browser/ui/webui/ash/login/core_oobe_handler.cc
@@ -226,7 +226,7 @@
 void CoreOobeHandler::HandleRaiseTabKeyEvent(bool reverse) {
   ui::KeyEvent event(ui::ET_KEY_PRESSED, ui::VKEY_TAB, ui::EF_NONE);
   if (reverse) {
-    event.set_flags(ui::EF_SHIFT_DOWN);
+    event.SetFlags(ui::EF_SHIFT_DOWN);
   }
   SendEventToSink(&event);
 }
diff --git a/chrome/browser/ui/webui/settings/people_handler.cc b/chrome/browser/ui/webui/settings/people_handler.cc
index 498ff02..59aef1c 100644
--- a/chrome/browser/ui/webui/settings/people_handler.cc
+++ b/chrome/browser/ui/webui/settings/people_handler.cc
@@ -4,6 +4,7 @@
 
 #include "chrome/browser/ui/webui/settings/people_handler.h"
 
+#include <memory>
 #include <optional>
 #include <string>
 
@@ -42,6 +43,7 @@
 #include "chrome/common/url_constants.h"
 #include "chrome/common/webui_url_constants.h"
 #include "chrome/grit/generated_resources.h"
+#include "components/prefs/pref_change_registrar.h"
 #include "components/prefs/pref_service.h"
 #include "components/signin/core/browser/signin_error_controller.h"
 #include "components/signin/public/base/consent_level.h"
@@ -323,8 +325,9 @@
 
 void PeopleHandler::OnJavascriptAllowed() {
   PrefService* prefs = profile_->GetPrefs();
-  profile_pref_registrar_.Init(prefs);
-  profile_pref_registrar_.Add(
+  profile_pref_registrar_ = std::make_unique<PrefChangeRegistrar>();
+  profile_pref_registrar_->Init(prefs);
+  profile_pref_registrar_->Add(
       prefs::kSigninAllowed,
       base::BindRepeating(&PeopleHandler::UpdateSyncStatus,
                           base::Unretained(this)));
@@ -343,7 +346,7 @@
 }
 
 void PeopleHandler::OnJavascriptDisallowed() {
-  profile_pref_registrar_.RemoveAll();
+  profile_pref_registrar_.reset();
   identity_manager_observation_.Reset();
   sync_service_observation_.Reset();
 }
diff --git a/chrome/browser/ui/webui/settings/people_handler.h b/chrome/browser/ui/webui/settings/people_handler.h
index 1a35db1..a598ab2 100644
--- a/chrome/browser/ui/webui/settings/people_handler.h
+++ b/chrome/browser/ui/webui/settings/people_handler.h
@@ -250,7 +250,7 @@
   std::unique_ptr<base::OneShotTimer> engine_start_timer_;
 
   // Used to listen for pref changes to allow or disallow signin.
-  PrefChangeRegistrar profile_pref_registrar_;
+  std::unique_ptr<PrefChangeRegistrar> profile_pref_registrar_;
 
   // Manages observer lifetimes.
   base::ScopedObservation<signin::IdentityManager,
diff --git a/chrome/browser/ui/webui/settings/performance_settings_interactive_uitest.cc b/chrome/browser/ui/webui/settings/performance_settings_interactive_uitest.cc
index 3caf94f..3bfc450 100644
--- a/chrome/browser/ui/webui/settings/performance_settings_interactive_uitest.cc
+++ b/chrome/browser/ui/webui/settings/performance_settings_interactive_uitest.cc
@@ -16,6 +16,7 @@
 #include "chrome/browser/ui/browser_finder.h"
 #include "chrome/browser/ui/performance_controls/test_support/battery_saver_browser_test_mixin.h"
 #include "chrome/browser/ui/performance_controls/test_support/memory_saver_interactive_test_mixin.h"
+#include "chrome/browser/ui/performance_controls/test_support/webui_interactive_test_mixin.h"
 #include "chrome/browser/ui/tabs/tab_enums.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/browser/ui/webui/feedback/feedback_dialog.h"
@@ -41,16 +42,9 @@
 namespace {
 DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kPerformanceSettingsPage);
 DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kSecondTabContent);
-DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kButtonWasClicked);
-DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementRenders);
 DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementHides);
-DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kIronCollapseContentShows);
 DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kExceptionDialogShows);
 
-constexpr char kCheckJsElementIsChecked[] = "(el) => { return el.checked; }";
-constexpr char kCheckJsElementIsNotChecked[] =
-    "(el) => { return !el.checked; }";
-
 const WebContentsInteractionTestUtil::DeepQuery kMemorySaverToggleQuery = {
     "settings-ui",
     "settings-main",
@@ -90,38 +84,14 @@
 }  // namespace
 
 class MemorySettingsInteractiveTest
-    : public MemorySaverInteractiveTestMixin<InteractiveBrowserTest> {
+    : public MemorySaverInteractiveTestMixin<
+          WebUiInteractiveTestMixin<InteractiveBrowserTest>> {
  public:
   void SetUpOnMainThread() override {
     MemorySaverInteractiveTestMixin::SetUpOnMainThread();
     SetMemorySaverModeEnabled(true);
   }
 
-  auto WaitForElementToRender(const ui::ElementIdentifier& contents_id,
-                              const DeepQuery& element) {
-    StateChange element_renders;
-    element_renders.event = kElementRenders;
-    element_renders.where = element;
-    element_renders.test_function =
-        "(el) => { if (el !== null) { let rect = el.getBoundingClientRect(); "
-        "return rect.width > 0 && rect.height > 0; } return false; }";
-
-    return WaitForStateChange(contents_id, element_renders);
-  }
-
-  auto ClickElement(const ui::ElementIdentifier& contents_id,
-                    const DeepQuery& element) {
-    return Steps(FlushEvents(), WaitForElementToRender(contents_id, element),
-                 MoveMouseTo(contents_id, element), ClickMouse());
-  }
-
-  auto CheckTabCount(int expected_tab_count) {
-    auto get_tab_count = base::BindLambdaForTesting(
-        [this]() { return browser()->tab_strip_model()->GetTabCount(); });
-
-    return CheckResult(get_tab_count, expected_tab_count);
-  }
-
   auto CheckMemorySaverModePrefState(MemorySaverModeState state) {
     return CheckResult(
         base::BindLambdaForTesting([]() {
@@ -142,32 +112,6 @@
     }));
   }
 
-  auto WaitForButtonStateChange(const ui::ElementIdentifier& contents_id,
-                                DeepQuery element,
-                                bool is_checked) {
-    StateChange toggle_selection_change;
-    toggle_selection_change.event = kButtonWasClicked;
-    toggle_selection_change.where = element;
-    toggle_selection_change.type = StateChange::Type::kExistsAndConditionTrue;
-    toggle_selection_change.test_function =
-        is_checked ? kCheckJsElementIsChecked : kCheckJsElementIsNotChecked;
-
-    return WaitForStateChange(contents_id, toggle_selection_change);
-  }
-
-  auto WaitForIronListCollapseStateChange(ui::ElementIdentifier webcontents_id,
-                                          DeepQuery query) {
-    StateChange iron_collapse_finish_animating;
-    iron_collapse_finish_animating.event = kIronCollapseContentShows;
-    iron_collapse_finish_animating.where = query;
-    iron_collapse_finish_animating.type =
-        StateChange::Type::kExistsAndConditionTrue;
-    iron_collapse_finish_animating.test_function =
-        "(el) => { return !el.transitioning; }";
-
-    return WaitForStateChange(webcontents_id, iron_collapse_finish_animating);
-  }
-
  private:
   base::test::ScopedFeatureList scoped_feature_list_;
 };
@@ -178,8 +122,8 @@
       NavigateWebContents(kPerformanceSettingsPage,
                           GURL(chrome::kChromeUIPerformanceSettingsURL)),
       WaitForElementToRender(kPerformanceSettingsPage, kMemorySaverToggleQuery),
-      CheckJsResultAt(kPerformanceSettingsPage, kMemorySaverToggleQuery,
-                      kCheckJsElementIsChecked),
+      WaitForButtonStateChange(kPerformanceSettingsPage,
+                               kMemorySaverToggleQuery, true),
 
       // Turn Off Memory Saver Mode
       ClickElement(kPerformanceSettingsPage, kMemorySaverToggleQuery),
@@ -210,7 +154,8 @@
                           GURL(chrome::kChromeUIPerformanceSettingsURL)),
       InstrumentNextTab(kLearnMorePage),
       ClickElement(kPerformanceSettingsPage, memory_saver_learn_more),
-      WaitForShow(kLearnMorePage), CheckTabCount(2),
+      WaitForShow(kLearnMorePage),
+      CheckResult([&]() { return browser()->tab_strip_model()->count(); }, 2),
       WaitForWebContentsReady(kLearnMorePage,
                               GURL(chrome::kMemorySaverModeLearnMoreUrl)));
 }
@@ -224,8 +169,8 @@
       NavigateWebContents(kPerformanceSettingsPage,
                           GURL(chrome::kChromeUIPerformanceSettingsURL)),
       WaitForElementToRender(kPerformanceSettingsPage, kMemorySaverToggleQuery),
-      CheckJsResultAt(kPerformanceSettingsPage, kMemorySaverToggleQuery,
-                      kCheckJsElementIsChecked),
+      WaitForButtonStateChange(kPerformanceSettingsPage,
+                               kMemorySaverToggleQuery, true),
 
       // Turn Off Memory Saver Mode
       ClickElement(kPerformanceSettingsPage, kMemorySaverToggleQuery),
@@ -293,8 +238,8 @@
       NavigateWebContents(kPerformanceSettingsPage,
                           GURL(chrome::kChromeUIPerformanceSettingsURL)),
       WaitForElementToRender(kPerformanceSettingsPage, kMemorySaverToggleQuery),
-      CheckJsResultAt(kPerformanceSettingsPage, kMemorySaverToggleQuery,
-                      kCheckJsElementIsChecked),
+      WaitForButtonStateChange(kPerformanceSettingsPage,
+                               kMemorySaverToggleQuery, true),
 
       // Enable memory saver mode to discard tabs based on a timer
       ClickElement(kPerformanceSettingsPage, kDiscardOnTimerQuery),
@@ -328,8 +273,8 @@
       NavigateWebContents(kPerformanceSettingsPage,
                           GURL(chrome::kChromeUIPerformanceSettingsURL)),
       WaitForElementToRender(kPerformanceSettingsPage, kMemorySaverToggleQuery),
-      CheckJsResultAt(kPerformanceSettingsPage, kMemorySaverToggleQuery,
-                      kCheckJsElementIsChecked),
+      WaitForButtonStateChange(kPerformanceSettingsPage,
+                               kMemorySaverToggleQuery, true),
 
       // Turn Off Memory Saver Mode
       ClickElement(kPerformanceSettingsPage, kMemorySaverToggleQuery),
@@ -393,8 +338,8 @@
       NavigateWebContents(kPerformanceSettingsPage,
                           GURL(chrome::kChromeUIPerformanceSettingsURL)),
       WaitForElementToRender(kPerformanceSettingsPage, kMemorySaverToggleQuery),
-      CheckJsResultAt(kPerformanceSettingsPage, kMemorySaverToggleQuery,
-                      kCheckJsElementIsChecked),
+      WaitForButtonStateChange(kPerformanceSettingsPage,
+                               kMemorySaverToggleQuery, true),
 
       // Select discard on timer option
       ClickElement(kPerformanceSettingsPage, kDiscardOnTimerQuery),
@@ -441,25 +386,14 @@
 
 #if !BUILDFLAG(IS_CHROMEOS)
 class BatterySettingsInteractiveTest
-    : public BatterySaverBrowserTestMixin<InteractiveBrowserTest> {
+    : public BatterySaverBrowserTestMixin<
+          WebUiInteractiveTestMixin<InteractiveBrowserTest>> {
  public:
   base::BatteryLevelProvider::BatteryState GetFakeBatteryState() override {
     return base::test::TestBatteryLevelProvider::CreateBatteryState(1, true,
                                                                     100);
   }
 
-  auto ClickElement(const ui::ElementIdentifier& contents_id,
-                    const DeepQuery& element) {
-    return Steps(MoveMouseTo(contents_id, element), ClickMouse());
-  }
-
-  auto CheckTabCount(int expected_tab_count) {
-    auto get_tab_count = base::BindLambdaForTesting(
-        [this]() { return browser()->tab_strip_model()->GetTabCount(); });
-
-    return CheckResult(get_tab_count, expected_tab_count);
-  }
-
   auto CheckBatteryStateLogged(const base::HistogramTester& histogram_tester,
                                BatterySaverModeState state,
                                int expected_count) {
@@ -470,44 +404,6 @@
     }));
   }
 
-  auto WaitForButtonStateChange(const ui::ElementIdentifier& contents_id,
-                                DeepQuery element,
-                                bool is_checked) {
-    StateChange toggle_selection_change;
-    toggle_selection_change.event = kButtonWasClicked;
-    toggle_selection_change.where = element;
-    toggle_selection_change.type = StateChange::Type::kExistsAndConditionTrue;
-    toggle_selection_change.test_function =
-        is_checked ? kCheckJsElementIsChecked : kCheckJsElementIsNotChecked;
-
-    return WaitForStateChange(contents_id, toggle_selection_change);
-  }
-
-  auto WaitForElementToRender(const ui::ElementIdentifier& contents_id,
-                              const DeepQuery& element) {
-    StateChange element_renders;
-    element_renders.event = kElementRenders;
-    element_renders.where = element;
-    element_renders.type = StateChange::Type::kExistsAndConditionTrue;
-    element_renders.test_function =
-        "(el) => { return el.clientWidth > 0 && el.clientHeight > 0; }";
-
-    return WaitForStateChange(contents_id, element_renders);
-  }
-
-  auto WaitForIronListCollapseStateChange(ui::ElementIdentifier webcontents_id,
-                                          DeepQuery query) {
-    StateChange iron_collapse_finish_animating;
-    iron_collapse_finish_animating.event = kIronCollapseContentShows;
-    iron_collapse_finish_animating.where = query;
-    iron_collapse_finish_animating.type =
-        StateChange::Type::kExistsAndConditionTrue;
-    iron_collapse_finish_animating.test_function =
-        "(el) => { return !el.transitioning; }";
-
-    return WaitForStateChange(webcontents_id, iron_collapse_finish_animating);
-  }
-
  private:
   base::test::ScopedFeatureList scoped_feature_list_;
 };
@@ -525,7 +421,8 @@
                           GURL(chrome::kChromeUIPerformanceSettingsURL)),
       InstrumentNextTab(kLearnMorePage),
       ClickElement(kPerformanceSettingsPage, battery_saver_learn_more),
-      WaitForShow(kLearnMorePage), CheckTabCount(2),
+      WaitForShow(kLearnMorePage),
+      CheckResult([&]() { return browser()->tab_strip_model()->count(); }, 2),
       WaitForWebContentsReady(kLearnMorePage,
                               GURL(chrome::kBatterySaverModeLearnMoreUrl)));
 }
@@ -555,8 +452,8 @@
       NavigateWebContents(kPerformanceSettingsPage,
                           GURL(chrome::kChromeUIPerformanceSettingsURL)),
       WaitForElementToRender(kPerformanceSettingsPage, battery_saver_toggle),
-      CheckJsResultAt(kPerformanceSettingsPage, battery_saver_toggle,
-                      kCheckJsElementIsChecked),
+      WaitForButtonStateChange(kPerformanceSettingsPage, battery_saver_toggle,
+                               true),
 
       // Turn off Battery Saver Mode
       ClickElement(kPerformanceSettingsPage, battery_saver_toggle),
@@ -610,7 +507,8 @@
 #endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
 #elif BUILDFLAG(IS_CHROMEOS_ASH)
-class BatterySettingsInteractiveTest : public InteractiveAshTest {
+class BatterySettingsInteractiveTest
+    : public WebUiInteractiveTestMixin<InteractiveAshTest> {
  public:
   BatterySettingsInteractiveTest()
       : scoped_feature_list_(ash::features::kBatterySaver) {}
@@ -621,25 +519,6 @@
             kForceDeviceHasBatterySwitch);
   }
 
-  auto WaitForElementToRender(const ui::ElementIdentifier& contents_id,
-                              const DeepQuery& element) {
-    StateChange element_renders;
-    element_renders.event = kElementRenders;
-    element_renders.where = element;
-    element_renders.type = StateChange::Type::kExistsAndConditionTrue;
-    element_renders.test_function =
-        "(el) => { return el !== null && el.clientWidth > 0 && el.clientHeight "
-        "> 0; }";
-
-    return WaitForStateChange(contents_id, element_renders);
-  }
-
-  auto ClickElement(const ui::ElementIdentifier& contents_id,
-                    const DeepQuery& element) {
-    return Steps(WaitForElementToRender(contents_id, element),
-                 MoveMouseTo(contents_id, element), ClickMouse());
-  }
-
  private:
   base::test::ScopedFeatureList scoped_feature_list_;
 };
@@ -672,13 +551,14 @@
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
 class TabDiscardExceptionsSettingsInteractiveTest
-    : public MemorySettingsInteractiveTest {
+    : public MemorySaverInteractiveTestMixin<
+          WebUiInteractiveTestMixin<InteractiveBrowserTest>> {
  public:
   void SetUp() override {
     scoped_feature_list_.InitAndEnableFeature(
         performance_manager::features::kDiscardExceptionsImprovements);
 
-    MemorySettingsInteractiveTest::SetUp();
+    MemorySaverInteractiveTestMixin::SetUp();
   }
 
   auto WaitForElementToHide(const ui::ElementIdentifier& contents_id,
@@ -735,10 +615,8 @@
     toggle_selection_change.event = kButtonWasClicked;
     toggle_selection_change.where = element;
     toggle_selection_change.type = StateChange::Type::kExistsAndConditionTrue;
-    toggle_selection_change.test_function =
-        is_disabled ? "(el) => el.disabled === true"
-                    : "(el) => el.disabled === false";
-
+    toggle_selection_change.test_function = base::StrCat(
+        {"(el) => el.disabled === ", is_disabled ? "true" : "false"});
     return WaitForStateChange(contents_id, toggle_selection_change);
   }
 
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 803c5d3..32a0b43 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1704304651-a17042d366bdc4880ce41120cbbea90368a15659.profdata
+chrome-win32-main-1704315597-fccaab2d242c7d5139573163711577cac00087c5.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 6740020..fb92dc6d 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1704304651-b9afcdcf1d41cfef4ad04b7b88c39224b9ee0f0d.profdata
+chrome-win64-main-1704315597-bd9b2d46d75914af5e2333c5f67c85ad3892be4d.profdata
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index c07091a..6c7b2713 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -660,6 +660,7 @@
       "../browser/ui/performance_controls/test_support/memory_saver_browser_test_mixin.h",
       "../browser/ui/performance_controls/test_support/memory_saver_interactive_test_mixin.h",
       "../browser/ui/performance_controls/test_support/user_education_browser_test_mixin.h",
+      "../browser/ui/performance_controls/test_support/webui_interactive_test_mixin.h",
       "../browser/ui/profiles/profile_ui_test_utils.h",
       "../browser/ui/safety_hub/safety_hub_test_util.cc",
       "../browser/ui/safety_hub/safety_hub_test_util.h",
diff --git a/chrome/test/data/webui/chromeos/BUILD.gn b/chrome/test/data/webui/chromeos/BUILD.gn
index 6fdcd43..7968405 100644
--- a/chrome/test/data/webui/chromeos/BUILD.gn
+++ b/chrome/test/data/webui/chromeos/BUILD.gn
@@ -180,7 +180,7 @@
     "fake_network_config_mojom.js",
     "fake_passpoint_service_mojom.ts",
     "internet_config_dialog_test.js",
-    "internet_detail_dialog_test.js",
+    "internet_detail_dialog_test.ts",
     "mock_controller.js",
     "mock_controller.m.js",
     "mojo_webui_test_support.js",
@@ -243,6 +243,9 @@
     "chrome://chrome-signin/arc_account_picker/*|" +
         rebase_path("//chrome/browser/resources/chromeos/arc_account_picker/*",
                     target_gen_dir),
+    "chrome://internet-detail-dialog/*|" + rebase_path(
+            "${root_gen_dir}/chrome/browser/resources/chromeos/internet_detail_dialog/tsc/*",
+            target_gen_dir),
     "chrome://webui-test/*|" +
         rebase_path("$root_gen_dir/chrome/test/data/webui/tsc/*",
                     target_gen_dir),
@@ -250,8 +253,10 @@
 
   deps = [
     "//ash/webui/common/resources:build_ts",
+    "//chrome/browser/resources/chromeos/internet_detail_dialog:build_ts",
     "//chrome/test/data/webui:build_ts",
     "//third_party/polymer/v3_0:library",
+    "//ui/webui/resources/cr_elements:build_ts",
     "//ui/webui/resources/js:build_ts",
     "//ui/webui/resources/mojo:build_ts",
   ]
diff --git a/chrome/test/data/webui/chromeos/ash_common/cr_elements/BUILD.gn b/chrome/test/data/webui/chromeos/ash_common/cr_elements/BUILD.gn
index 8691314..f5d18fe 100644
--- a/chrome/test/data/webui/chromeos/ash_common/cr_elements/BUILD.gn
+++ b/chrome/test/data/webui/chromeos/ash_common/cr_elements/BUILD.gn
@@ -10,14 +10,20 @@
     "cr_action_menu_test.ts",
     "cr_button_test.ts",
     "cr_checkbox_test.ts",
+    "cr_container_shadow_mixin_test.ts",
     "cr_dialog_test.ts",
     "cr_expand_button_test.ts",
     "cr_icon_button_test.ts",
     "cr_input_test.ts",
     "cr_toast_manager_test.ts",
     "cr_toast_test.ts",
+    "i18n_mixin_test.ts",
   ]
 
+  # Using custom config to turn off useDefineForClassFields TS compiler flag
+  # which is necessary when defining Polymer elements.
+  ts_tsconfig_base = "tsconfig_base.json"
+
   ts_deps = [
     "//ash/webui/common/resources/cr_elements:build_ts",
     "//third_party/polymer/v3_0:library",
diff --git a/chrome/test/data/webui/chromeos/ash_common/cr_elements/ash_common_cr_elements_browsertest.cc b/chrome/test/data/webui/chromeos/ash_common/cr_elements/ash_common_cr_elements_browsertest.cc
index 3d21466c..aa611939 100644
--- a/chrome/test/data/webui/chromeos/ash_common/cr_elements/ash_common_cr_elements_browsertest.cc
+++ b/chrome/test/data/webui/chromeos/ash_common/cr_elements/ash_common_cr_elements_browsertest.cc
@@ -16,6 +16,11 @@
   RunTest("chromeos/ash_common/cr_elements/cr_button_test.js", "mocha.run()");
 }
 
+IN_PROC_BROWSER_TEST_F(AshCommonCrElementsTest, CrContainerShadowMixin) {
+  RunTest("chromeos/ash_common/cr_elements/cr_container_shadow_mixin_test.js",
+          "mocha.run()");
+}
+
 IN_PROC_BROWSER_TEST_F(AshCommonCrElementsTest, CrDialog) {
   RunTest("chromeos/ash_common/cr_elements/cr_dialog_test.js", "mocha.run()");
 }
@@ -38,3 +43,7 @@
   RunTest("chromeos/ash_common/cr_elements/cr_toast_manager_test.js",
           "mocha.run()");
 }
+
+IN_PROC_BROWSER_TEST_F(AshCommonCrElementsTest, I18nMixin) {
+  RunTest("chromeos/ash_common/cr_elements/i18n_mixin_test.js", "mocha.run()");
+}
diff --git a/chrome/test/data/webui/chromeos/ash_common/cr_elements/cr_container_shadow_mixin_test.ts b/chrome/test/data/webui/chromeos/ash_common/cr_elements/cr_container_shadow_mixin_test.ts
new file mode 100644
index 0000000..6c34b04a
--- /dev/null
+++ b/chrome/test/data/webui/chromeos/ash_common/cr_elements/cr_container_shadow_mixin_test.ts
@@ -0,0 +1,74 @@
+// Copyright 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// clang-format off
+import {CrContainerShadowMixin} from 'chrome://resources/ash/common/cr_elements/cr_container_shadow_mixin.js';
+import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
+// clang-format on
+
+suite('CrContainerShadowBehavior', function() {
+  const TestElementBase = CrContainerShadowMixin(PolymerElement);
+
+  class TestElement extends TestElementBase {
+    static get is() {
+      return 'test-element';
+    }
+
+    static get template() {
+      return html`
+         <style>
+           #container {
+             height: 50px;
+           }
+         </style>
+         <div id="before"></div>
+         <div id="container" show-bottom-shadow$="[[showBottomShadow]]"></div>
+         <div id="after"></div>
+       `;
+    }
+
+    static get properties() {
+      return {
+        showBottomShadow: Boolean,
+      };
+    }
+
+    showBottomShadow: boolean = false;
+  }
+
+  customElements.define(TestElement.is, TestElement);
+
+  setup(function() {
+    document.body.innerHTML = window.trustedTypes!.emptyHTML;
+  });
+
+  test('no bottom shadow', function() {
+    const element = document.createElement('test-element') as TestElement;
+    document.body.appendChild(element);
+
+    // Should not have a bottom shadow div.
+    assertFalse(
+        !!element.shadowRoot!.querySelector('#cr-container-shadow-bottom'));
+    assertTrue(!!element.shadowRoot!.querySelector('#cr-container-shadow-top'));
+
+    element.showBottomShadow = true;
+
+    // Still no bottom shadow since this is only checked in connectedCallback();
+    assertFalse(
+        !!element.shadowRoot!.querySelector('#cr-container-shadow-bottom'));
+    assertTrue(!!element.shadowRoot!.querySelector('#cr-container-shadow-top'));
+  });
+
+  test('show bottom shadow', function() {
+    const element = document.createElement('test-element') as TestElement;
+    element.showBottomShadow = true;
+    document.body.appendChild(element);
+
+    // Has both shadows.
+    assertTrue(
+        !!element.shadowRoot!.querySelector('#cr-container-shadow-bottom'));
+    assertTrue(!!element.shadowRoot!.querySelector('#cr-container-shadow-top'));
+  });
+});
diff --git a/chrome/test/data/webui/chromeos/ash_common/cr_elements/i18n_mixin_test.ts b/chrome/test/data/webui/chromeos/ash_common/cr_elements/i18n_mixin_test.ts
new file mode 100644
index 0000000..7964dc9e
--- /dev/null
+++ b/chrome/test/data/webui/chromeos/ash_common/cr_elements/i18n_mixin_test.ts
@@ -0,0 +1,74 @@
+// Copyright 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
+import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
+import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {assertEquals, assertFalse, assertThrows, assertTrue} from 'chrome://webui-test/chai_assert.js';
+
+const TestElementBase = I18nMixin(PolymerElement);
+class TestElement extends TestElementBase {}
+customElements.define('test-element', TestElement);
+
+suite('I18nMixinTest', function() {
+  const allowedByDefault = '<a href="https://google.com">Google!</a>';
+  const text = 'I\'m just text, nobody should have a problem with me!';
+  const nonBreakingSpace = 'A\u00a0B\u00a0C';  // \u00a0 is a unicode nbsp.
+
+  let testElement: TestElement;
+
+  suiteSetup(function() {
+    loadTimeData.data = {
+      'allowedByDefault': allowedByDefault,
+      'customAttr': '<a is="action-link">Take action!</a>',
+      'optionalTag': '<img>',
+      'javascriptHref': '<a href="javascript:alert(1)">teh hax</a>',
+      'script': '<script>alert(/xss/)</scr' +
+          'ipt>',
+      'text': text,
+      'nonBreakingSpace': nonBreakingSpace,
+    };
+  });
+
+  setup(function() {
+    document.body.innerHTML = window.trustedTypes!.emptyHTML;
+    testElement = document.createElement('test-element') as TestElement;
+    document.body.appendChild(testElement);
+  });
+
+  test('i18n', function() {
+    assertEquals(text, testElement.i18n('text'));
+    assertEquals(nonBreakingSpace, testElement.i18n('nonBreakingSpace'));
+
+    assertThrows(function() {
+      testElement.i18n('customAttr');
+    });
+    assertThrows(function() {
+      testElement.i18n('optionalTag');
+    });
+    assertThrows(function() {
+      testElement.i18n('javascriptHref');
+    });
+    assertThrows(function() {
+      testElement.i18n('script');
+    });
+  });
+
+  test('i18n advanced', function() {
+    assertEquals(
+        allowedByDefault,
+        testElement.i18nAdvanced('allowedByDefault').toString());
+    testElement.i18nAdvanced('customAttr', {attrs: ['is']});
+    testElement.i18nAdvanced('optionalTag', {tags: ['img']});
+  });
+
+  test('i18n dynamic', function() {
+    assertEquals(text, testElement.i18nDynamic('en', 'text'));
+  });
+
+  test('i18n exists', function() {
+    assertTrue(testElement.i18nExists('text'));
+    assertFalse(testElement.i18nExists('missingText'));
+  });
+});
diff --git a/chrome/test/data/webui/chromeos/ash_common/cr_elements/tsconfig_base.json b/chrome/test/data/webui/chromeos/ash_common/cr_elements/tsconfig_base.json
new file mode 100644
index 0000000..b2ec655
--- /dev/null
+++ b/chrome/test/data/webui/chromeos/ash_common/cr_elements/tsconfig_base.json
@@ -0,0 +1,6 @@
+{
+  "extends": "../tsconfig_base.json",
+  "compilerOptions": {
+    "useDefineForClassFields": false
+  }
+}
diff --git a/chrome/test/data/webui/chromeos/cloud_upload/BUILD.gn b/chrome/test/data/webui/chromeos/cloud_upload/BUILD.gn
index 75f19f6..119e3da1 100644
--- a/chrome/test/data/webui/chromeos/cloud_upload/BUILD.gn
+++ b/chrome/test/data/webui/chromeos/cloud_upload/BUILD.gn
@@ -25,7 +25,6 @@
     "//chrome/browser/resources/chromeos/cloud_upload:build_ts",
     "//chrome/test/data/webui/chromeos:build_ts",
     "//third_party/cros-components:cros_components_ts",
-    "//ui/webui/resources/cr_elements:build_ts",
     "//ui/webui/resources/js:build_ts",
   ]
 }
diff --git a/chrome/test/data/webui/chromeos/internet_detail_dialog_test.js b/chrome/test/data/webui/chromeos/internet_detail_dialog_test.js
deleted file mode 100644
index 1f59db0..0000000
--- a/chrome/test/data/webui/chromeos/internet_detail_dialog_test.js
+++ /dev/null
@@ -1,542 +0,0 @@
-// Copyright 2021 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'chrome://internet-detail-dialog/internet_detail_dialog.js';
-
-import {InternetDetailDialogBrowserProxyImpl} from 'chrome://internet-detail-dialog/internet_detail_dialog_browser_proxy.js';
-import {MojoInterfaceProviderImpl} from 'chrome://resources/ash/common/network/mojo_interface_provider.js';
-import {OncMojo} from 'chrome://resources/ash/common/network/onc_mojo.js';
-import {CrosNetworkConfigRemote, InhibitReason, MAX_NUM_CUSTOM_APNS} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
-import {ConnectionStateType, DeviceStateType, NetworkType, OncSource, PortalState} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
-import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
-import {FakeNetworkConfig} from 'chrome://webui-test/chromeos/fake_network_config_mojom.js';
-import {TestBrowserProxy} from 'chrome://webui-test/test_browser_proxy.js';
-
-/** @implements {InternetDetailDialogBrowserProxy} */
-export class TestInternetDetailDialogBrowserProxy extends TestBrowserProxy {
-  constructor() {
-    super([
-      'getDialogArguments',
-      'closeDialog',
-      'showPortalSignin',
-    ]);
-  }
-
-  /** @override */
-  getDialogArguments() {
-    return JSON.stringify({guid: 'guid'});
-  }
-
-  /** @override */
-  closeDialog() {}
-
-  /** @override */
-  showPortalSignin() {}
-}
-
-suite('internet-detail-dialog', () => {
-  const guid = 'guid';
-  const test_iccid = '11111111111111111';
-  let internetDetailDialog = null;
-
-  /** @type {?CrosNetworkConfigRemote} */
-  let mojoApi_;
-
-  suiteSetup(function() {
-    mojoApi_ = new FakeNetworkConfig();
-    MojoInterfaceProviderImpl.getInstance().remote_ = mojoApi_;
-  });
-
-  function flushAsync() {
-    flush();
-    // Use setTimeout to wait for the next macrotask.
-    return new Promise(resolve => setTimeout(resolve));
-  }
-
-  function getManagedProperties(type, opt_source) {
-    const result = OncMojo.getDefaultManagedProperties(type, guid, name);
-    if (opt_source) {
-      result.source = opt_source;
-    }
-    return result;
-  }
-
-  setup(async () => {
-    PolymerTest.clearBody();
-    InternetDetailDialogBrowserProxyImpl.setInstance(
-        new TestInternetDetailDialogBrowserProxy());
-    mojoApi_.resetForTest();
-  });
-
-  teardown(function() {
-    // If a previous test was run with Jelly, the css needs to be removed.
-    const old_elements =
-        document.querySelectorAll('link[href*=\'chrome://theme/colors.css\']');
-    old_elements.forEach(function(node) {
-      node.remove();
-    });
-    assertFalse(
-        !!document.querySelector('link[href*=\'chrome://theme/colors.css\']'));
-
-    document.body.classList.remove('jelly-enabled');
-  });
-
-  async function init() {
-    internetDetailDialog = document.createElement('internet-detail-dialog');
-    document.body.appendChild(internetDetailDialog);
-    await flushAsync();
-  }
-
-  async function setupCellularNetwork(
-      isPrimary, isInhibited, connectedApn, customApnList, errorState,
-      portalState) {
-    await mojoApi_.setNetworkTypeEnabledState(NetworkType.kCellular, true);
-
-    const cellularNetwork =
-        getManagedProperties(NetworkType.kCellular, OncSource.kDevice);
-    cellularNetwork.typeProperties.cellular.iccid = test_iccid;
-    // Required for connectDisconnectButton to be rendered.
-    cellularNetwork.connectionState = isPrimary ?
-        ConnectionStateType.kConnected :
-        ConnectionStateType.kNotConnected;
-    // Required for networkChooseMobile to be rendered.
-    cellularNetwork.typeProperties.cellular.supportNetworkScan = true;
-    cellularNetwork.typeProperties.cellular.connectedApn = connectedApn;
-    cellularNetwork.typeProperties.cellular.customApnList = customApnList;
-    cellularNetwork.errorState = errorState;
-    cellularNetwork.portalState = portalState;
-
-    mojoApi_.setManagedPropertiesForTest(cellularNetwork);
-    mojoApi_.setDeviceStateForTest({
-      type: NetworkType.kCellular,
-      deviceState: DeviceStateType.kEnabled,
-      inhibitReason:
-          (isInhibited ? InhibitReason.kInstallingProfile :
-                         InhibitReason.kNotInhibited),
-      simInfos: [{
-        iccid: test_iccid,
-        isPrimary: isPrimary,
-      }],
-    });
-  }
-
-  function getElement(selector) {
-    const element = internetDetailDialog.$$(selector);
-    assertTrue(!!element);
-    return element;
-  }
-
-  suite('captive portal ui updates', () => {
-    function getButton(buttonId) {
-      const button =
-          internetDetailDialog.shadowRoot.querySelector(`#${buttonId}`);
-      assertTrue(!!button);
-      return button;
-    }
-
-    test('WiFi in a portal portalState', function() {
-      mojoApi_.setNetworkTypeEnabledState(NetworkType.kWiFi, true);
-      const wifiNetwork = getManagedProperties(NetworkType.kWiFi, 'wifi_user');
-      wifiNetwork.source = OncSource.kUser;
-      wifiNetwork.connectable = true;
-      wifiNetwork.connectionState = ConnectionStateType.kPortal;
-      wifiNetwork.portalState = PortalState.kPortal;
-
-      mojoApi_.setManagedPropertiesForTest(wifiNetwork);
-      init();
-      return flushAsync().then(() => {
-        const networkStateText =
-            internetDetailDialog.shadowRoot.querySelector(`#networkState`);
-        assertTrue(networkStateText.hasAttribute('warning'));
-        assertEquals(
-            networkStateText.textContent.trim(),
-            internetDetailDialog.i18n('networkListItemSignIn'));
-        const signinButton = getButton('signinButton');
-        assertTrue(!!signinButton);
-        assertFalse(signinButton.hasAttribute('hidden'));
-        assertFalse(signinButton.disabled);
-      });
-    });
-
-    test('WiFi in a no internet portalState', function() {
-      mojoApi_.setNetworkTypeEnabledState(NetworkType.kWiFi, true);
-      const wifiNetwork = getManagedProperties(NetworkType.kWiFi, 'wifi_user');
-      wifiNetwork.source = OncSource.kUser;
-      wifiNetwork.connectable = true;
-      wifiNetwork.connectionState = ConnectionStateType.kPortal;
-      wifiNetwork.portalState = PortalState.kNoInternet;
-
-      mojoApi_.setManagedPropertiesForTest(wifiNetwork);
-      init();
-      return flushAsync().then(() => {
-        const networkStateText =
-            internetDetailDialog.shadowRoot.querySelector(`#networkState`);
-        assertTrue(networkStateText.hasAttribute('warning'));
-        assertEquals(
-            networkStateText.textContent.trim(),
-            internetDetailDialog.i18n(
-                'networkListItemConnectedNoConnectivity'));
-        const signinButton = getButton('signinButton');
-        assertTrue(!!signinButton);
-        assertTrue(signinButton.hasAttribute('hidden'));
-        assertTrue(signinButton.disabled);
-      });
-    });
-
-    test('WiFi in a proxy-auth portalState', function() {
-      mojoApi_.setNetworkTypeEnabledState(NetworkType.kWiFi, true);
-      const wifiNetwork = getManagedProperties(NetworkType.kWiFi, 'wifi_user');
-      wifiNetwork.source = OncSource.kUser;
-      wifiNetwork.connectable = true;
-      wifiNetwork.connectionState = ConnectionStateType.kPortal;
-      wifiNetwork.portalState = PortalState.kProxyAuthRequired;
-
-      mojoApi_.setManagedPropertiesForTest(wifiNetwork);
-      init();
-      return flushAsync().then(() => {
-        const networkStateText =
-            internetDetailDialog.shadowRoot.querySelector(`#networkState`);
-        assertTrue(networkStateText.hasAttribute('warning'));
-        assertEquals(
-            networkStateText.textContent.trim(),
-            internetDetailDialog.i18n('networkListItemSignIn'));
-        const signinButton = getButton('signinButton');
-        assertTrue(!!signinButton);
-        assertFalse(signinButton.hasAttribute('hidden'));
-        assertFalse(signinButton.disabled);
-      });
-    });
-  });
-
-  test('Network not on active sim, hide configurations', async () => {
-    await setupCellularNetwork(/*isPrimary=*/ false, /*isInhibited=*/ false);
-
-    await init();
-    assertFalse(internetDetailDialog.showConfigurableSections_);
-
-    const managedProperties = internetDetailDialog.managedProperties_;
-    assertTrue(internetDetailDialog.showCellularSim_(managedProperties));
-    assertFalse(!!internetDetailDialog.$$('network-siminfo'));
-
-    // The 'Forget' and 'ConnectDisconnect' buttons should still be showing.
-    assertTrue(!!internetDetailDialog.$$('cr-button'));
-  });
-
-  test('Network on active sim, show configurations', async () => {
-    await setupCellularNetwork(/*isPrimary=*/ true, /*isInhibited=*/ false);
-
-    await init();
-    assertTrue(internetDetailDialog.showConfigurableSections_);
-
-    const managedProperties = internetDetailDialog.managedProperties_;
-    assertTrue(internetDetailDialog.showCellularSim_(managedProperties));
-    assertTrue(!!internetDetailDialog.$$('network-siminfo'));
-  });
-
-  test('Dialog disabled when inhibited', async () => {
-    // Start uninhibited.
-    await setupCellularNetwork(/*isPrimary=*/ true, /*isInhibited=*/ false);
-    await init();
-
-    const connectDisconnectButton = getElement('#connectDisconnect');
-    const networkSimInfo = getElement('network-siminfo');
-    const networkChooseMobile = getElement('network-choose-mobile');
-    const networkApnlist = getElement('network-apnlist');
-    const networkProxy = getElement('network-proxy');
-    const networkIpConfig = getElement('network-ip-config');
-    const networkNameservers = getElement('network-nameservers');
-    const infoFields = getElement('network-property-list-mojo');
-
-    assertFalse(connectDisconnectButton.disabled);
-    assertFalse(networkSimInfo.disabled);
-    assertFalse(networkChooseMobile.disabled);
-    assertFalse(networkApnlist.disabled);
-    assertTrue(networkProxy.editable);
-    assertFalse(networkIpConfig.disabled);
-    assertFalse(networkNameservers.disabled);
-    assertFalse(infoFields.disabled);
-
-    // Mock device being inhibited.
-    mojoApi_.setDeviceStateForTest({
-      type: NetworkType.kCellular,
-      deviceState: DeviceStateType.kEnabled,
-      inhibitReason: InhibitReason.kInstallingProfile,
-      simInfos: [{
-        iccid: test_iccid,
-        isPrimary: true,
-      }],
-    });
-    await flushAsync();
-
-    assertTrue(connectDisconnectButton.disabled);
-    assertTrue(networkSimInfo.disabled);
-    assertTrue(networkChooseMobile.disabled);
-    assertTrue(networkApnlist.disabled);
-    assertFalse(networkProxy.editable);
-    assertTrue(networkIpConfig.disabled);
-    assertTrue(networkNameservers.disabled);
-    assertTrue(infoFields.disabled);
-
-    // Uninhibit.
-    mojoApi_.setDeviceStateForTest({
-      type: NetworkType.kCellular,
-      deviceState: DeviceStateType.kEnabled,
-      inhibitReason: InhibitReason.kNotInhibited,
-      simInfos: [{
-        iccid: test_iccid,
-        isPrimary: true,
-      }],
-    });
-    await flushAsync();
-
-    assertFalse(connectDisconnectButton.disabled);
-    assertFalse(networkSimInfo.disabled);
-    assertFalse(networkChooseMobile.disabled);
-    assertFalse(networkApnlist.disabled);
-    assertTrue(networkProxy.editable);
-    assertFalse(networkIpConfig.disabled);
-    assertFalse(networkNameservers.disabled);
-    assertFalse(infoFields.disabled);
-  });
-
-  // Syntactic sugar for running test twice with different values for the
-  // apnRevamp feature flag.
-  [true, false].forEach(isApnRevampEnabled => {
-    test('Show/Hide APN row correspondingly to ApnRevamp flag', async () => {
-      loadTimeData.overrideValues({
-        apnRevamp: isApnRevampEnabled,
-      });
-      const errorState = 'invalid-apn';
-      await setupCellularNetwork(
-          /* isPrimary= */ true, /* isInhibited= */ false,
-          /* connectedApn= */ undefined, /* customApnList= */ undefined,
-          errorState, PortalState.kNoInternet);
-
-      await init();
-      const legacyApnElement =
-          internetDetailDialog.shadowRoot.querySelector('network-apnlist');
-      const apnSection =
-          internetDetailDialog.shadowRoot.querySelector('cr-expand-button');
-
-      if (isApnRevampEnabled) {
-        assertFalse(!!legacyApnElement);
-        assertTrue(!!apnSection);
-        assertEquals(
-            internetDetailDialog.i18n('internetApnPageTitle'),
-            internetDetailDialog.shadowRoot.querySelector('#apnRowTitle')
-                .textContent);
-        const getApnSectionSublabel = () =>
-            internetDetailDialog.shadowRoot.querySelector('#apnRowSublabel')
-                .textContent.trim();
-        assertFalse(!!getApnSectionSublabel());
-        const getApnList = () =>
-            internetDetailDialog.shadowRoot.querySelector('apn-list');
-        assertTrue(!!getApnList());
-        assertTrue(getApnList().shouldOmitLinks);
-        assertEquals(errorState, getApnList().errorState);
-        assertEquals(PortalState.kNoInternet, getApnList().portalState);
-        const isApnListShowing = () =>
-            internetDetailDialog.shadowRoot.querySelector('iron-collapse')
-                .opened;
-        assertFalse(isApnListShowing());
-
-        // Add a connected APN.
-        const accessPointName = 'access point name';
-        await setupCellularNetwork(
-            /* isPrimary= */ true, /* isInhibited= */ false,
-            {accessPointName: accessPointName});
-
-        // Force a refresh.
-        internetDetailDialog.onDeviceStateListChanged();
-        await flushAsync();
-        assertEquals(accessPointName, getApnSectionSublabel());
-        assertFalse(
-            internetDetailDialog.shadowRoot.querySelector('#apnRowSublabel')
-                .hasAttribute('warning'));
-        assertFalse(isApnListShowing());
-
-        // Update the APN's name property and add a restricted connectivity
-        // state.
-        const name = 'name';
-        await setupCellularNetwork(
-            /* isPrimary= */ true, /* isInhibited= */ false,
-            {accessPointName: accessPointName, name: name},
-            /* customApnList= */ undefined, /* errorState= */ undefined,
-            PortalState.kNoInternet);
-
-        // Force a refresh.
-        internetDetailDialog.onDeviceStateListChanged();
-        await flushAsync();
-        assertEquals(name, getApnSectionSublabel());
-        assertTrue(
-            internetDetailDialog.shadowRoot.querySelector('#apnRowSublabel')
-                .hasAttribute('warning'));
-        assertFalse(isApnListShowing());
-
-        // Expand the section, the sublabel should no longer show.
-        apnSection.click();
-        await flushAsync();
-        assertFalse(!!getApnSectionSublabel());
-        assertTrue(isApnListShowing());
-
-        // Collapse the section, the sublabel should show.
-        apnSection.click();
-        await flushAsync();
-        assertEquals(name, getApnSectionSublabel());
-        assertFalse(isApnListShowing());
-      } else {
-        assertTrue(!!legacyApnElement);
-        assertFalse(!!apnSection);
-      }
-    });
-  });
-
-  test(
-      'Disable and show tooltip for New APN button when custom APNs limit is' +
-          ' reached',
-      async () => {
-        loadTimeData.overrideValues({
-          apnRevamp: true,
-        });
-        await setupCellularNetwork(
-            /* isPrimary= */ true, /* isInhibited= */ false,
-            {accessPointName: 'access point name'}, []);
-        await init();
-        internetDetailDialog.shadowRoot.querySelector('cr-expand-button')
-            .click();
-
-        const getApnButton = () =>
-            internetDetailDialog.shadowRoot.querySelector(
-                '#createCustomApnButton');
-        const getApnTooltip = () =>
-            internetDetailDialog.shadowRoot.querySelector('#apnTooltip');
-
-        assertTrue(!!getApnButton());
-        assertFalse(!!getApnTooltip());
-        assertFalse(getApnButton().disabled);
-
-        // We're setting the list of APNs to the max number
-        await setupCellularNetwork(
-            /* isPrimary= */ true, /* isInhibited= */ false,
-            {accessPointName: 'access point name'},
-            Array.apply(null, {length: MAX_NUM_CUSTOM_APNS}).map(_ => {
-              return {
-                accessPointName: 'apn',
-              };
-            }));
-        internetDetailDialog.onDeviceStateListChanged();
-        await flushAsync();
-
-        assertTrue(!!getApnTooltip());
-        assertTrue(getApnButton().disabled);
-        assertTrue(getApnTooltip().innerHTML.includes(
-            internetDetailDialog.i18n('customApnLimitReached')));
-
-        await setupCellularNetwork(
-            /* isPrimary= */ true, /* isInhibited= */ false,
-            {accessPointName: 'access point name'}, []);
-        internetDetailDialog.onDeviceStateListChanged();
-        await flushAsync();
-
-        assertFalse(!!getApnTooltip());
-        assertFalse(getApnButton().disabled);
-
-        getApnButton().click();
-        await flushAsync();
-        assertTrue(!!internetDetailDialog.shadowRoot.querySelector('apn-list')
-                         .shadowRoot.querySelector('apn-detail-dialog'));
-      });
-
-  [false, true].forEach(isJellyEnabled => {
-    test('Dynamic theme CSS is added when isJellyEnabled is set', async () => {
-      loadTimeData.overrideValues({
-        isJellyEnabled: isJellyEnabled,
-      });
-      await setupCellularNetwork(
-          /*isPrimary=*/ true, /*isInhibited=*/ false);
-      await init();
-
-      const linkEl =
-          document.querySelector('link[href*=\'chrome://theme/colors.css\']');
-      if (isJellyEnabled) {
-        assertTrue(!!linkEl);
-        assertTrue(document.body.classList.contains('jelly-enabled'));
-      } else {
-        assertEquals(null, linkEl);
-        assertFalse(document.body.classList.contains('jelly-enabled'));
-      }
-    });
-  });
-
-  test('Show toast on show-error-toast event', async function() {
-    loadTimeData.overrideValues({
-      apnRevamp: true,
-    });
-    await init();
-    const getErrorToast = () =>
-        internetDetailDialog.shadowRoot.querySelector('#errorToast');
-    assertFalse(getErrorToast().open);
-
-    const message = 'Toast message';
-    const event = new CustomEvent('show-error-toast', {detail: message});
-    internetDetailDialog.dispatchEvent(event);
-    await flushAsync();
-    assertTrue(getErrorToast().open);
-    assertEquals(
-        internetDetailDialog.shadowRoot.querySelector('#errorToastMessage')
-            .innerHTML,
-        message);
-  });
-
-  test(
-      'Dont show toast on show-error-toast event when ApnRevamp false',
-      async function() {
-        loadTimeData.overrideValues({
-          apnRevamp: false,
-        });
-        await init();
-        const getErrorToast = () =>
-            internetDetailDialog.shadowRoot.querySelector('#errorToast');
-        assertFalse(!!getErrorToast());
-
-        const message = 'Toast message';
-        const event = new CustomEvent('show-error-toast', {detail: message});
-        internetDetailDialog.dispatchEvent(event);
-        await flushAsync();
-        assertFalse(!!getErrorToast());
-      });
-
-  test('MacAddress not shown when invalid', async function() {
-    mojoApi_.setNetworkTypeEnabledState(NetworkType.kWiFi, true);
-    const wifiNetwork = getManagedProperties(NetworkType.kWiFi, 'wifi_user');
-    wifiNetwork.source = OncSource.kUser;
-    wifiNetwork.connectable = true;
-    wifiNetwork.connectionState = ConnectionStateType.kConnected;
-
-    mojoApi_.setManagedPropertiesForTest(wifiNetwork);
-    mojoApi_.setDeviceStateForTest({
-      type: NetworkType.kWiFi,
-      deviceState: DeviceStateType.kEnabled,
-      macAddress: '01:10:10:10:10:10',
-    });
-    await flushAsync();
-
-    init();
-    await flushAsync();
-    let macAddress = getElement('#macAddress');
-
-    assertTrue(!!macAddress);
-    assertFalse(macAddress.hidden);
-
-    mojoApi_.setDeviceStateForTest({
-      type: NetworkType.kWiFi,
-      deviceState: DeviceStateType.kEnabled,
-      macAddress: '00:00:00:00:00:00',
-    });
-    await flushAsync();
-
-    macAddress = getElement('#macAddress');
-    assertTrue(macAddress.hidden);
-  });
-});
diff --git a/chrome/test/data/webui/chromeos/internet_detail_dialog_test.ts b/chrome/test/data/webui/chromeos/internet_detail_dialog_test.ts
new file mode 100644
index 0000000..6f05992
--- /dev/null
+++ b/chrome/test/data/webui/chromeos/internet_detail_dialog_test.ts
@@ -0,0 +1,601 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'chrome://internet-detail-dialog/internet_detail_dialog.js';
+
+import {InternetDetailDialogElement} from 'chrome://internet-detail-dialog/internet_detail_dialog.js';
+import {InternetDetailDialogBrowserProxy, InternetDetailDialogBrowserProxyImpl} from 'chrome://internet-detail-dialog/internet_detail_dialog_browser_proxy.js';
+import {ApnList} from 'chrome://resources/ash/common/network/apn_list.js';
+import {MojoInterfaceProviderImpl} from 'chrome://resources/ash/common/network/mojo_interface_provider.js';
+import {NetworkApnListElement} from 'chrome://resources/ash/common/network/network_apnlist.js';
+import {NetworkChooseMobileElement} from 'chrome://resources/ash/common/network/network_choose_mobile.js';
+import {NetworkIpConfigElement} from 'chrome://resources/ash/common/network/network_ip_config.js';
+import {NetworkNameserversElement} from 'chrome://resources/ash/common/network/network_nameservers.js';
+import {NetworkPropertyListMojoElement} from 'chrome://resources/ash/common/network/network_property_list_mojo.js';
+import {NetworkProxyElement} from 'chrome://resources/ash/common/network/network_proxy.js';
+import {NetworkSiminfoElement} from 'chrome://resources/ash/common/network/network_siminfo.js';
+import {OncMojo} from 'chrome://resources/ash/common/network/onc_mojo.js';
+import {CrButtonElement} from 'chrome://resources/cr_elements/cr_button/cr_button.js';
+import {CrToastElement} from 'chrome://resources/cr_elements/cr_toast/cr_toast.js';
+import {assert} from 'chrome://resources/js/assert.js';
+import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
+import {ApnAuthenticationType, ApnIpType, ApnProperties, ApnState, ApnType, InhibitReason, MAX_NUM_CUSTOM_APNS, SIMInfo} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
+import {ConnectionStateType, DeviceStateType, NetworkType, OncSource, PortalState} from 'chrome://resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
+import {IronCollapseElement} from 'chrome://resources/polymer/v3_0/iron-collapse/iron-collapse.js';
+import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
+import {TestBrowserProxy} from 'chrome://webui-test/test_browser_proxy.js';
+
+import {FakeNetworkConfig} from './fake_network_config_mojom.js';
+
+export class TestInternetDetailDialogBrowserProxy extends TestBrowserProxy
+    implements InternetDetailDialogBrowserProxy {
+  constructor() {
+    super([
+      'getDialogArguments',
+      'closeDialog',
+      'showPortalSignin',
+    ]);
+  }
+
+  getDialogArguments() {
+    return JSON.stringify({guid: 'guid'});
+  }
+
+  closeDialog() {}
+
+  showPortalSignin() {}
+}
+
+suite('internet-detail-dialog', () => {
+  const guid = 'guid';
+  const testIccid = '11111111111111111';
+  let internetDetailDialog: InternetDetailDialogElement;
+
+  let mojoApi: FakeNetworkConfig;
+
+  suiteSetup(function() {
+    mojoApi = new FakeNetworkConfig();
+    MojoInterfaceProviderImpl.getInstance().setMojoServiceRemoteForTest(
+        mojoApi);
+  });
+
+  function flushAsync() {
+    flush();
+    // Use setTimeout to wait for the next macrotask.
+    return new Promise(resolve => setTimeout(resolve));
+  }
+
+  function getManagedProperties(
+      type: NetworkType, name?: string, source?: OncSource) {
+    const result =
+        OncMojo.getDefaultManagedProperties(type, guid, name ? name : '');
+    if (source) {
+      result.source = source;
+    }
+    return result;
+  }
+
+  setup(async () => {
+    assert(window.trustedTypes);
+    document.body.innerHTML = window.trustedTypes.emptyHTML;
+    InternetDetailDialogBrowserProxyImpl.setInstance(
+        new TestInternetDetailDialogBrowserProxy());
+    mojoApi.resetForTest();
+  });
+
+  teardown(function() {
+    // If a previous test was run with Jelly, the css needs to be removed.
+    const old_elements =
+        document.querySelectorAll('link[href*=\'chrome://theme/colors.css\']');
+    old_elements.forEach(function(node) {
+      node.remove();
+    });
+    assertFalse(
+        !!document.querySelector('link[href*=\'chrome://theme/colors.css\']'));
+
+    document.body.classList.remove('jelly-enabled');
+  });
+
+  async function init() {
+    internetDetailDialog = document.createElement('internet-detail-dialog');
+    document.body.appendChild(internetDetailDialog);
+    await flushAsync();
+  }
+
+  async function setupCellularNetwork(
+      isPrimary: boolean, isInhibited: boolean, connectedApn?: ApnProperties,
+      customApnList?: ApnProperties[], errorState?: string,
+      portalState?: PortalState) {
+    await mojoApi.setNetworkTypeEnabledState(NetworkType.kCellular, true);
+
+    const cellularNetwork = getManagedProperties(
+        NetworkType.kCellular, /*name=*/ undefined, OncSource.kDevice);
+    if (cellularNetwork.typeProperties.cellular) {
+      cellularNetwork.typeProperties.cellular.iccid = testIccid;
+      // Required for networkChooseMobile to be rendered.
+      cellularNetwork.typeProperties.cellular.supportNetworkScan = true;
+      cellularNetwork.typeProperties.cellular.connectedApn = connectedApn;
+      cellularNetwork.typeProperties.cellular.customApnList = customApnList;
+    }
+    // Required for connectDisconnectButton to be rendered.
+    cellularNetwork.connectionState = isPrimary ?
+        ConnectionStateType.kConnected :
+        ConnectionStateType.kNotConnected;
+    cellularNetwork.errorState = errorState;
+    if (portalState) {
+      cellularNetwork.portalState = portalState;
+    }
+
+    mojoApi.setManagedPropertiesForTest(cellularNetwork);
+    setDeviceState(
+        NetworkType.kCellular, DeviceStateType.kEnabled,
+        (isInhibited ? InhibitReason.kInstallingProfile :
+                       InhibitReason.kNotInhibited),
+        [{
+          iccid: testIccid,
+          isPrimary: isPrimary,
+          slotId: 1,
+          eid: 'eid',
+        }]);
+  }
+
+  function setDeviceState(
+      type: NetworkType, deviceState: DeviceStateType,
+      inhibitReason?: InhibitReason, simInfos?: SIMInfo[],
+      macAddress?: string) {
+    mojoApi.setDeviceStateForTest({
+      type: type,
+      deviceState: deviceState,
+      inhibitReason: inhibitReason ? inhibitReason :
+                                     InhibitReason.kNotInhibited,
+      simInfos: simInfos ? simInfos : undefined,
+      ipv4Address: undefined,
+      ipv6Address: undefined,
+      imei: undefined,
+      macAddress: macAddress,
+      scanning: false,
+      simLockStatus: undefined,
+      simAbsent: false,
+      managedNetworkAvailable: false,
+      serial: undefined,
+      isCarrierLocked: false,
+    });
+  }
+
+  function createApn(accessPointName: string, name?: string) {
+    return {
+      accessPointName: accessPointName,
+      id: undefined,
+      authentication: ApnAuthenticationType.kAutomatic,
+      language: undefined,
+      localizedName: undefined,
+      name: name,
+      password: undefined,
+      username: undefined,
+      attach: undefined,
+      state: ApnState.kEnabled,
+      ipType: ApnIpType.kAutomatic,
+      apnTypes: [ApnType.kDefault],
+    };
+  }
+
+  function getElement<T extends HTMLElement = HTMLElement>(selector: string):
+      T {
+    const element = internetDetailDialog.shadowRoot!.querySelector<T>(selector);
+    assert(element);
+    return element;
+  }
+
+  suite('captive portal ui updates', () => {
+    function getButton(buttonId: string): CrButtonElement {
+      const button =
+          internetDetailDialog.shadowRoot!.querySelector<CrButtonElement>(
+              `#${buttonId}`);
+      assertTrue(!!button);
+      return button;
+    }
+
+    test('WiFi in a portal portalState', function() {
+      mojoApi.setNetworkTypeEnabledState(NetworkType.kWiFi, true);
+      const wifiNetwork = getManagedProperties(NetworkType.kWiFi, 'wifi_user');
+      wifiNetwork.source = OncSource.kUser;
+      wifiNetwork.connectable = true;
+      wifiNetwork.connectionState = ConnectionStateType.kPortal;
+      wifiNetwork.portalState = PortalState.kPortal;
+
+      mojoApi.setManagedPropertiesForTest(wifiNetwork);
+      init();
+      return flushAsync().then(() => {
+        const networkStateText = getElement('#networkState');
+        assertTrue(networkStateText.hasAttribute('warning'));
+        assert(networkStateText.textContent);
+        assertEquals(
+            networkStateText.textContent.trim(),
+            internetDetailDialog.i18n('networkListItemSignIn'));
+        const signinButton = getButton('signinButton');
+        assertTrue(!!signinButton);
+        assertFalse(signinButton.hasAttribute('hidden'));
+        assertFalse(signinButton.disabled);
+      });
+    });
+
+    test('WiFi in a no internet portalState', function() {
+      mojoApi.setNetworkTypeEnabledState(NetworkType.kWiFi, true);
+      const wifiNetwork = getManagedProperties(NetworkType.kWiFi, 'wifi_user');
+      wifiNetwork.source = OncSource.kUser;
+      wifiNetwork.connectable = true;
+      wifiNetwork.connectionState = ConnectionStateType.kPortal;
+      wifiNetwork.portalState = PortalState.kNoInternet;
+
+      mojoApi.setManagedPropertiesForTest(wifiNetwork);
+      init();
+      return flushAsync().then(() => {
+        const networkStateText = getElement('#networkState');
+        assertTrue(networkStateText.hasAttribute('warning'));
+        assert(networkStateText.textContent);
+        assertEquals(
+            networkStateText.textContent.trim(),
+            internetDetailDialog.i18n(
+                'networkListItemConnectedNoConnectivity'));
+        const signinButton = getButton('signinButton');
+        assertTrue(!!signinButton);
+        assertTrue(signinButton.hasAttribute('hidden'));
+        assertTrue(signinButton.disabled);
+      });
+    });
+
+    test('WiFi in a proxy-auth portalState', function() {
+      mojoApi.setNetworkTypeEnabledState(NetworkType.kWiFi, true);
+      const wifiNetwork = getManagedProperties(NetworkType.kWiFi, 'wifi_user');
+      wifiNetwork.source = OncSource.kUser;
+      wifiNetwork.connectable = true;
+      wifiNetwork.connectionState = ConnectionStateType.kPortal;
+      wifiNetwork.portalState = PortalState.kProxyAuthRequired;
+
+      mojoApi.setManagedPropertiesForTest(wifiNetwork);
+      init();
+      return flushAsync().then(() => {
+        const networkStateText = getElement('#networkState');
+        assertTrue(networkStateText.hasAttribute('warning'));
+        assert(networkStateText.textContent);
+        assertEquals(
+            networkStateText.textContent.trim(),
+            internetDetailDialog.i18n('networkListItemSignIn'));
+        const signinButton = getButton('signinButton');
+        assertTrue(!!signinButton);
+        assertFalse(signinButton.hasAttribute('hidden'));
+        assertFalse(signinButton.disabled);
+      });
+    });
+  });
+
+  test('Network not on active sim, hide configurations', async () => {
+    await setupCellularNetwork(/*isPrimary=*/ false, /*isInhibited=*/ false);
+
+    await init();
+    assertFalse(
+        !!internetDetailDialog.shadowRoot!.querySelector<HTMLElement>('.hr'));
+
+    assertFalse(!!internetDetailDialog.shadowRoot!.querySelector<HTMLElement>(
+        'network-siminfo'));
+
+    // The 'Forget' and 'ConnectDisconnect' buttons should still be showing.
+    assertTrue(!!internetDetailDialog.shadowRoot!.querySelector<HTMLElement>(
+        'cr-button'));
+  });
+
+  test('Network on active sim, show configurations', async () => {
+    await setupCellularNetwork(/*isPrimary=*/ true, /*isInhibited=*/ false);
+
+    await init();
+    assertTrue(
+        !!internetDetailDialog.shadowRoot!.querySelector<HTMLElement>('.hr'));
+
+    assertTrue(!!internetDetailDialog.shadowRoot!.querySelector<HTMLElement>(
+        'network-siminfo'));
+  });
+
+  test('Dialog disabled when inhibited', async () => {
+    // Start uninhibited.
+    await setupCellularNetwork(/*isPrimary=*/ true, /*isInhibited=*/ false);
+    await init();
+
+    const connectDisconnectButton =
+        getElement<CrButtonElement>('#connectDisconnect');
+    const networkSimInfo = getElement<NetworkSiminfoElement>('network-siminfo');
+    const networkChooseMobile =
+        getElement<NetworkChooseMobileElement>('network-choose-mobile');
+    const networkApnlist = getElement<NetworkApnListElement>('network-apnlist');
+    const networkProxy = getElement<NetworkProxyElement>('network-proxy');
+    const networkIpConfig =
+        getElement<NetworkIpConfigElement>('network-ip-config');
+    const networkNameservers =
+        getElement<NetworkNameserversElement>('network-nameservers');
+    const infoFields = getElement<NetworkPropertyListMojoElement>(
+        'network-property-list-mojo');
+
+    assertFalse(connectDisconnectButton.disabled);
+    assertFalse(networkSimInfo.disabled);
+    assertFalse(networkChooseMobile.disabled);
+    assertFalse(networkApnlist.disabled);
+    assertTrue(networkProxy.editable);
+    assertFalse(networkIpConfig.disabled);
+    assertFalse(networkNameservers.disabled);
+    assertFalse(infoFields.disabled);
+
+    // Mock device being inhibited.
+    setDeviceState(
+        NetworkType.kCellular,
+        DeviceStateType.kEnabled,
+        InhibitReason.kInstallingProfile,
+        [{
+          iccid: testIccid,
+          isPrimary: true,
+          slotId: 1,
+          eid: 'eid',
+        }],
+    );
+    await flushAsync();
+
+    assertTrue(connectDisconnectButton.disabled);
+    assertTrue(networkSimInfo.disabled);
+    assertTrue(networkChooseMobile.disabled);
+    assertTrue(networkApnlist.disabled);
+    assertFalse(networkProxy.editable);
+    assertTrue(networkIpConfig.disabled);
+    assertTrue(networkNameservers.disabled);
+    assertTrue(infoFields.disabled);
+
+    // Uninhibit.
+    setDeviceState(
+        NetworkType.kCellular,
+        DeviceStateType.kEnabled,
+        InhibitReason.kNotInhibited,
+        [{
+          iccid: testIccid,
+          isPrimary: true,
+          slotId: 1,
+          eid: 'eid',
+        }],
+    );
+    await flushAsync();
+
+    assertFalse(connectDisconnectButton.disabled);
+    assertFalse(networkSimInfo.disabled);
+    assertFalse(networkChooseMobile.disabled);
+    assertFalse(networkApnlist.disabled);
+    assertTrue(networkProxy.editable);
+    assertFalse(networkIpConfig.disabled);
+    assertFalse(networkNameservers.disabled);
+    assertFalse(infoFields.disabled);
+  });
+
+  // Syntactic sugar for running test twice with different values for the
+  // apnRevamp feature flag.
+  [true, false].forEach(isApnRevampEnabled => {
+    test('Show/Hide APN row correspondingly to ApnRevamp flag', async () => {
+      loadTimeData.overrideValues({
+        apnRevamp: isApnRevampEnabled,
+      });
+      const errorState = 'invalid-apn';
+      await setupCellularNetwork(
+          /* isPrimary= */ true, /* isInhibited= */ false,
+          /* connectedApn= */ undefined, /* customApnList= */ undefined,
+          errorState, PortalState.kNoInternet);
+
+      await init();
+      const legacyApnElement =
+          internetDetailDialog.shadowRoot!.querySelector('network-apnlist');
+      const apnSection =
+          internetDetailDialog.shadowRoot!.querySelector('cr-expand-button');
+
+      if (isApnRevampEnabled) {
+        assertFalse(!!legacyApnElement);
+        assertTrue(!!apnSection);
+        assertEquals(
+            internetDetailDialog.i18n('internetApnPageTitle'),
+            getElement('#apnRowTitle').textContent);
+        const apnRowSublabel = getElement('#apnRowSublabel');
+        const getApnSectionSublabel = () => {
+          assert(apnRowSublabel.textContent);
+          return apnRowSublabel.textContent.trim();
+        };
+
+        assertFalse(!!getApnSectionSublabel());
+        const getApnList = () => getElement<ApnList>('apn-list');
+        assertTrue(getApnList().shouldOmitLinks);
+        assertEquals(errorState, getApnList().errorState);
+        assertEquals(PortalState.kNoInternet, getApnList().portalState);
+        const isApnListShowing = () =>
+            getElement<IronCollapseElement>('iron-collapse').opened;
+
+        assertFalse(isApnListShowing());
+
+        // Add a connected APN.
+        const accessPointName = 'access point name';
+        await setupCellularNetwork(
+            /* isPrimary= */ true, /* isInhibited= */ false,
+            createApn(accessPointName));
+
+        // Force a refresh.
+        internetDetailDialog.onDeviceStateListChanged();
+        await flushAsync();
+        assertEquals(accessPointName, getApnSectionSublabel());
+        assertFalse(apnRowSublabel.hasAttribute('warning'));
+        assertFalse(isApnListShowing());
+
+        // Update the APN's name property and add a restricted connectivity
+        // state.
+        const name = 'name';
+        await setupCellularNetwork(
+            /* isPrimary= */ true, /* isInhibited= */ false,
+            createApn(accessPointName, name),
+            /* customApnList= */ undefined, /* errorState= */ undefined,
+            PortalState.kNoInternet);
+
+        // Force a refresh.
+        internetDetailDialog.onDeviceStateListChanged();
+        await flushAsync();
+        assertEquals(name, getApnSectionSublabel());
+        assertTrue(apnRowSublabel.hasAttribute('warning'));
+        assertFalse(isApnListShowing());
+
+        // Expand the section, the sublabel should no longer show.
+        apnSection.click();
+        await flushAsync();
+        assertFalse(!!getApnSectionSublabel());
+        assertTrue(isApnListShowing());
+
+        // Collapse the section, the sublabel should show.
+        apnSection.click();
+        await flushAsync();
+        assertEquals(name, getApnSectionSublabel());
+        assertFalse(isApnListShowing());
+      } else {
+        assertTrue(!!legacyApnElement);
+        assertFalse(!!apnSection);
+      }
+    });
+  });
+
+  test(
+      'Disable and show tooltip for New APN button when custom APNs limit is' +
+          ' reached',
+      async () => {
+        loadTimeData.overrideValues({
+          apnRevamp: true,
+        });
+        await setupCellularNetwork(
+            /* isPrimary= */ true, /* isInhibited= */ false,
+            createApn(/*accessPointName=*/ 'access point name'), []);
+        await init();
+        getElement('cr-expand-button').click();
+
+        const getApnButton = () =>
+            getElement<CrButtonElement>('#createCustomApnButton');
+
+        const getApnTooltip = () =>
+            internetDetailDialog.shadowRoot!.querySelector('#apnTooltip');
+
+        assertTrue(!!getApnButton());
+        assertFalse(!!getApnTooltip());
+        assertFalse(getApnButton().disabled);
+
+        // We're setting the list of APNs to the max number
+        await setupCellularNetwork(
+            /* isPrimary= */ true, /* isInhibited= */ false,
+            createApn(/*accessPointName=*/ 'access point name'),
+            Array(MAX_NUM_CUSTOM_APNS)
+                .fill(createApn(/*accessPointName=*/ 'apn')));
+        internetDetailDialog.onDeviceStateListChanged();
+        await flushAsync();
+
+        assertTrue(getApnButton().disabled);
+        const apnTooltip = getApnTooltip();
+        assert(apnTooltip);
+        assertTrue(apnTooltip.innerHTML.includes(
+            internetDetailDialog.i18n('customApnLimitReached')));
+
+        await setupCellularNetwork(
+            /* isPrimary= */ true, /* isInhibited= */ false,
+            createApn(/*accessPointName=*/ 'access point name'), []);
+        internetDetailDialog.onDeviceStateListChanged();
+        await flushAsync();
+
+        assertFalse(!!getApnTooltip());
+        assertFalse(getApnButton().disabled);
+
+        getApnButton().click();
+        await flushAsync();
+        assertTrue(!!getElement('apn-list')
+                         .shadowRoot!.querySelector('apn-detail-dialog'));
+      });
+
+  [false, true].forEach(isJellyEnabled => {
+    test('Dynamic theme CSS is added when isJellyEnabled is set', async () => {
+      loadTimeData.overrideValues({
+        isJellyEnabled: isJellyEnabled,
+      });
+      await setupCellularNetwork(
+          /*isPrimary=*/ true, /*isInhibited=*/ false);
+      await init();
+
+      const linkEl =
+          document.querySelector('link[href*=\'chrome://theme/colors.css\']');
+      if (isJellyEnabled) {
+        assertTrue(!!linkEl);
+        assertTrue(document.body.classList.contains('jelly-enabled'));
+      } else {
+        assertEquals(null, linkEl);
+        assertFalse(document.body.classList.contains('jelly-enabled'));
+      }
+    });
+  });
+
+  test('Show toast on show-error-toast event', async function() {
+    loadTimeData.overrideValues({
+      apnRevamp: true,
+    });
+    await init();
+    const getErrorToast = () => getElement<CrToastElement>('#errorToast');
+    assertFalse(getErrorToast().open);
+
+    const message = 'Toast message';
+    const event = new CustomEvent('show-error-toast', {detail: message});
+    internetDetailDialog.dispatchEvent(event);
+    await flushAsync();
+    assertTrue(getErrorToast().open);
+    assertEquals(getElement('#errorToastMessage').innerHTML, message);
+  });
+
+  test(
+      'Dont show toast on show-error-toast event when ApnRevamp false',
+      async function() {
+        loadTimeData.overrideValues({
+          apnRevamp: false,
+        });
+        await init();
+        const getErrorToast = () =>
+            internetDetailDialog.shadowRoot!.querySelector('#errorToast');
+        assertFalse(!!getErrorToast());
+
+        const message = 'Toast message';
+        const event = new CustomEvent('show-error-toast', {detail: message});
+        internetDetailDialog.dispatchEvent(event);
+        await flushAsync();
+        assertFalse(!!getErrorToast());
+      });
+
+  test('MacAddress not shown when invalid', async function() {
+    mojoApi.setNetworkTypeEnabledState(NetworkType.kWiFi, true);
+    const wifiNetwork = getManagedProperties(NetworkType.kWiFi, 'wifi_user');
+    wifiNetwork.source = OncSource.kUser;
+    wifiNetwork.connectable = true;
+    wifiNetwork.connectionState = ConnectionStateType.kConnected;
+
+    mojoApi.setManagedPropertiesForTest(wifiNetwork);
+    setDeviceState(
+        NetworkType.kWiFi, DeviceStateType.kEnabled,
+        /*inhibitReason=*/ undefined, /*simInfos=*/ undefined,
+        /*macAddress=*/ '01:10:10:10:10:10');
+    await flushAsync();
+
+    init();
+    await flushAsync();
+    let macAddress = getElement('#macAddress');
+
+    assertTrue(!!macAddress);
+    assertFalse(macAddress.hidden);
+
+    setDeviceState(
+        NetworkType.kWiFi, DeviceStateType.kEnabled,
+        /*inhibitReason=*/ undefined, /*simInfos=*/ undefined,
+        /*macAddress=*/ '00:00:00:00:00:00');
+    await flushAsync();
+
+    macAddress = getElement('#macAddress');
+    assertTrue(macAddress.hidden);
+  });
+});
diff --git a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/activation_code_page_test.js b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/activation_code_page_test.js
index 7611590..d481e96 100644
--- a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/activation_code_page_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/activation_code_page_test.js
@@ -101,8 +101,9 @@
     await enumerateDeviceResolvedPromise;
   }
 
-  test('Page description', async function () {
-    const description = activationCodePage.$$('#description');
+  test('Page description', async function() {
+    const description =
+        activationCodePage.shadowRoot.querySelector('#description');
     assertTrue(!!description);
 
     // Mock camera on
@@ -115,7 +116,7 @@
 
     // Clearing devices to test without camera
     mediaDevices.removeDevice();
-    await resolveEnumeratedDevicesPromise();
+      await resolveEnumeratedDevicesPromise();
     activationCodePage.showNoProfilesFound = true;
     assertEquals(description.innerText.trim(),
       loadTimeData.getString('enterActivationCodeNoProfilesFound'));
@@ -125,20 +126,27 @@
   });
 
   test('UI states', async function() {
-    let qrCodeDetectorContainer = activationCodePage.$$('#esimQrCodeDetection');
+    let qrCodeDetectorContainer =
+        activationCodePage.shadowRoot.querySelector('#esimQrCodeDetection');
     const activationCodeContainer =
-        activationCodePage.$$('#activationCodeContainer');
-    const video = activationCodePage.$$('#video');
+        activationCodePage.shadowRoot.querySelector('#activationCodeContainer');
+    const video = activationCodePage.shadowRoot.querySelector('#video');
     const startScanningContainer =
-        activationCodePage.$$('#startScanningContainer');
-    const startScanningButton = activationCodePage.$$('#startScanningButton');
-    const scanFinishContainer = activationCodePage.$$('#scanFinishContainer');
-    const switchCameraButton = activationCodePage.$$('#switchCameraButton');
+        activationCodePage.shadowRoot.querySelector('#startScanningContainer');
+    const startScanningButton =
+        activationCodePage.shadowRoot.querySelector('#startScanningButton');
+    const scanFinishContainer =
+        activationCodePage.shadowRoot.querySelector('#scanFinishContainer');
+    const switchCameraButton =
+        activationCodePage.shadowRoot.querySelector('#switchCameraButton');
     const getUseCameraAgainButton = () => {
-      return activationCodePage.$$('#useCameraAgainButton');
+      return activationCodePage.shadowRoot.querySelector(
+          '#useCameraAgainButton');
     };
-    const scanSuccessContainer = activationCodePage.$$('#scanSuccessContainer');
-    const scanFailureContainer = activationCodePage.$$('#scanFailureContainer');
+    const scanSuccessContainer =
+        activationCodePage.shadowRoot.querySelector('#scanSuccessContainer');
+    const scanFailureContainer =
+        activationCodePage.shadowRoot.querySelector('#scanFailureContainer');
 
     assertTrue(!!qrCodeDetectorContainer);
     assertTrue(!!activationCodeContainer);
@@ -150,7 +158,8 @@
     assertFalse(!!getUseCameraAgainButton());
     assertTrue(!!scanSuccessContainer);
     assertTrue(!!scanFailureContainer);
-    assertFalse(!!activationCodePage.$$('paper-spinner-lite'));
+    assertFalse(
+        !!activationCodePage.shadowRoot.querySelector('paper-spinner-lite'));
 
     // Initial state should only be showing the start scanning UI.
     assertFalse(startScanningContainer.hidden);
@@ -158,7 +167,8 @@
     assertTrue(video.hidden);
     assertTrue(scanFinishContainer.hidden);
     assertTrue(switchCameraButton.hidden);
-    assertFalse(!!activationCodePage.$$('paper-spinner-lite'));
+    assertFalse(
+        !!activationCodePage.shadowRoot.querySelector('paper-spinner-lite'));
 
     // Click the start scanning button.
     startScanningButton.click();
@@ -191,7 +201,7 @@
     assertFalse(activationCodePage.showError);
 
     // Simulate typing in the input.
-    activationCodePage.$$('#activationCode')
+    activationCodePage.shadowRoot.querySelector('#activationCode')
         .dispatchEvent(new KeyboardEvent('keydown', {key: 'A'}));
     await flushAsync();
 
@@ -201,11 +211,13 @@
     assertTrue(video.hidden);
     assertTrue(scanFinishContainer.hidden);
     assertTrue(switchCameraButton.hidden);
-    assertFalse(!!activationCodePage.$$('paper-spinner-lite'));
+    assertFalse(
+        !!activationCodePage.shadowRoot.querySelector('paper-spinner-lite'));
 
     activationCodePage.showBusy = true;
     await flushAsync();
-    assertTrue(!!activationCodePage.$$('paper-spinner-lite'));
+    assertTrue(
+        !!activationCodePage.shadowRoot.querySelector('paper-spinner-lite'));
 
     // Mock, no media devices present
     mediaDevices.removeDevice();
@@ -213,15 +225,18 @@
 
     // When no camera device is present qrCodeDetector container should
     // not be shown
-    qrCodeDetectorContainer = activationCodePage.$$('#esimQrCodeDetection');
+    qrCodeDetectorContainer =
+        activationCodePage.shadowRoot.querySelector('#esimQrCodeDetection');
 
     assertFalse(!!qrCodeDetectorContainer);
   });
 
   test('Switch camera button states', async function() {
-    const video = activationCodePage.$$('#video');
-    const startScanningButton = activationCodePage.$$('#startScanningButton');
-    const switchCameraButton = activationCodePage.$$('#switchCameraButton');
+    const video = activationCodePage.shadowRoot.querySelector('#video');
+    const startScanningButton =
+        activationCodePage.shadowRoot.querySelector('#startScanningButton');
+    const switchCameraButton =
+        activationCodePage.shadowRoot.querySelector('#switchCameraButton');
 
     assertTrue(!!video);
     assertTrue(!!startScanningButton);
@@ -290,10 +305,14 @@
   });
 
   test('UI is disabled when showBusy property is set', async function() {
-    const startScanningButton = activationCodePage.$$('#startScanningButton');
-    const switchCameraButton = activationCodePage.$$('#switchCameraButton');
-    const tryAgainButton = activationCodePage.$$('#tryAgainButton');
-    const input = activationCodePage.$$('#activationCode');
+    const startScanningButton =
+        activationCodePage.shadowRoot.querySelector('#startScanningButton');
+    const switchCameraButton =
+        activationCodePage.shadowRoot.querySelector('#switchCameraButton');
+    const tryAgainButton =
+        activationCodePage.shadowRoot.querySelector('#tryAgainButton');
+    const input =
+        activationCodePage.shadowRoot.querySelector('#activationCode');
 
     assertTrue(!!startScanningButton);
     assertTrue(!!switchCameraButton);
@@ -317,11 +336,12 @@
       'Do not show qrContainer when BarcodeDetector is not ready',
       async function() {
         let qrCodeDetectorContainer =
-            activationCodePage.$$('#esimQrCodeDetection');
+            activationCodePage.shadowRoot.querySelector('#esimQrCodeDetection');
 
         assertTrue(!!qrCodeDetectorContainer);
         // Activation code input should be at the bottom of the page.
-        assertTrue(activationCodePage.$$('#activationCodeContainer')
+        assertTrue(activationCodePage.shadowRoot
+                       .querySelector('#activationCodeContainer')
                        .classList.contains('relative'));
 
         FakeBarcodeDetector.setShouldFail(true);
@@ -329,11 +349,13 @@
             FakeBarcodeDetector, FakeImageCapture, setIntervalFunction,
             playVideoFunction, stopStreamFunction);
 
-        qrCodeDetectorContainer = activationCodePage.$$('#esimQrCodeDetection');
+        qrCodeDetectorContainer =
+            activationCodePage.shadowRoot.querySelector('#esimQrCodeDetection');
 
         assertFalse(!!qrCodeDetectorContainer);
         // Activation code input should now be in the center of the page.
-        assertTrue(activationCodePage.$$('#activationCodeContainer')
+        assertTrue(activationCodePage.shadowRoot
+                       .querySelector('#activationCodeContainer')
                        .classList.contains('center'));
       });
 
@@ -342,7 +364,8 @@
     activationCodePage.addEventListener('forward-navigation-requested', () => {
       eventFired = true;
     });
-    const input = activationCodePage.$$('#activationCode');
+    const input =
+        activationCodePage.shadowRoot.querySelector('#activationCode');
     input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
 
     await flushAsync();
@@ -352,11 +375,13 @@
   test(
       'Install error after manual entry should show error on input',
       async function() {
-        const input = activationCodePage.$$('#activationCode');
+        const input =
+            activationCodePage.shadowRoot.querySelector('#activationCode');
         const startScanningContainer =
-            activationCodePage.$$('#startScanningContainer');
+            activationCodePage.shadowRoot.querySelector(
+                '#startScanningContainer');
         const scanFinishContainer =
-            activationCodePage.$$('#scanFinishContainer');
+            activationCodePage.shadowRoot.querySelector('#scanFinishContainer');
         assertTrue(!!input);
         assertTrue(!!startScanningContainer);
         assertTrue(!!scanFinishContainer);
@@ -374,18 +399,23 @@
   test(
       'Install error after scanning should show error on camera',
       async function() {
-        const input = activationCodePage.$$('#activationCode');
+        const input =
+            activationCodePage.shadowRoot.querySelector('#activationCode');
         const startScanningContainer =
-            activationCodePage.$$('#startScanningContainer');
+            activationCodePage.shadowRoot.querySelector(
+                '#startScanningContainer');
         const startScanningButton =
-            activationCodePage.$$('#startScanningButton');
+            activationCodePage.shadowRoot.querySelector('#startScanningButton');
         const scanFinishContainer =
-            activationCodePage.$$('#scanFinishContainer');
+            activationCodePage.shadowRoot.querySelector('#scanFinishContainer');
         const scanInstallFailureHeader =
-            activationCodePage.$$('#scanInstallFailureHeader');
-        const scanSucessHeader = activationCodePage.$$('#scanSucessHeader');
+            activationCodePage.shadowRoot.querySelector(
+                '#scanInstallFailureHeader');
+        const scanSucessHeader =
+            activationCodePage.shadowRoot.querySelector('#scanSucessHeader');
         const getUseCameraAgainButton = () => {
-          return activationCodePage.$$('#useCameraAgainButton');
+          return activationCodePage.shadowRoot.querySelector(
+              '#useCameraAgainButton');
         };
         assertTrue(!!input);
         assertTrue(!!startScanningContainer);
@@ -428,9 +458,12 @@
       });
 
   test('Tabbing does not close video stream', async function() {
-    const startScanningButton = activationCodePage.$$('#startScanningButton');
-    const getVideo = () => activationCodePage.$$('#video');
-    const input = activationCodePage.$$('#activationCode');
+    const startScanningButton =
+        activationCodePage.shadowRoot.querySelector('#startScanningButton');
+    const getVideo = () =>
+        activationCodePage.shadowRoot.querySelector('#video');
+    const input =
+        activationCodePage.shadowRoot.querySelector('#activationCode');
 
     assertTrue(!!startScanningButton);
     assertTrue(!!getVideo());
@@ -460,8 +493,9 @@
   test(
       'Clear qr code detection timeout when video is hidden', async function() {
         const startScanningButton =
-            activationCodePage.$$('#startScanningButton');
-        const getVideo = () => activationCodePage.$$('#video');
+            activationCodePage.shadowRoot.querySelector('#startScanningButton');
+        const getVideo = () =>
+            activationCodePage.shadowRoot.querySelector('#video');
 
         assertTrue(!!startScanningButton);
         assertTrue(!!getVideo());
@@ -483,7 +517,8 @@
       });
 
   test('Input entered manually is validated', async function() {
-    const input = activationCodePage.$$('#activationCode');
+    const input =
+        activationCodePage.shadowRoot.querySelector('#activationCode');
     assertTrue(!!input);
     assertFalse(input.invalid);
 
@@ -537,16 +572,22 @@
   });
 
   test('Scanned code is validated', async function() {
-    const input = activationCodePage.$$('#activationCode');
+    const input =
+        activationCodePage.shadowRoot.querySelector('#activationCode');
     const startScanningContainer =
-        activationCodePage.$$('#startScanningContainer');
-    const startScanningButton = activationCodePage.$$('#startScanningButton');
-    const scanFinishContainer = activationCodePage.$$('#scanFinishContainer');
+        activationCodePage.shadowRoot.querySelector('#startScanningContainer');
+    const startScanningButton =
+        activationCodePage.shadowRoot.querySelector('#startScanningButton');
+    const scanFinishContainer =
+        activationCodePage.shadowRoot.querySelector('#scanFinishContainer');
     const scanInstallFailureHeader =
-        activationCodePage.$$('#scanInstallFailureHeader');
-    const scanSucessHeader = activationCodePage.$$('#scanSucessHeader');
+        activationCodePage.shadowRoot.querySelector(
+            '#scanInstallFailureHeader');
+    const scanSucessHeader =
+        activationCodePage.shadowRoot.querySelector('#scanSucessHeader');
     const getUseCameraAgainButton = () => {
-      return activationCodePage.$$('#useCameraAgainButton');
+      return activationCodePage.shadowRoot.querySelector(
+          '#useCameraAgainButton');
     };
     assertTrue(!!input);
     assertTrue(!!startScanningContainer);
@@ -628,7 +669,8 @@
   });
 
   test('check carrier lock warning', async function() {
-    assertTrue(!!activationCodePage.$$('#carrierLockWarningContainer'));
+    assertTrue(!!activationCodePage.shadowRoot.querySelector(
+        '#carrierLockWarningContainer'));
   });
 
   test(
@@ -641,7 +683,8 @@
         });
         await flushAsync();
         const page = document.createElement('activation-code-page');
-        assertFalse(!!page.$$('#carrierLockWarningContainer'));
+        assertFalse(
+            !!page.shadowRoot?.querySelector('#carrierLockWarningContainer'));
       });
 
   test(
@@ -655,6 +698,7 @@
         });
         await flushAsync();
         const page = document.createElement('activation-code-page');
-        assertFalse(!!page.$$('#carrierLockWarningContainer'));
+        assertFalse(
+            !!page.shadowRoot?.querySelector('#carrierLockWarningContainer'));
       });
 });
diff --git a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/base_page_test.js b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/base_page_test.js
index a7c1756f..093c7f79 100644
--- a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/base_page_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/base_page_test.js
@@ -17,25 +17,25 @@
   test('Title is shown', function() {
     basePage.title = 'Base page titile';
     flush();
-    const title = basePage.$$('#title');
+    const title = basePage.shadowRoot.querySelector('#title');
     assertTrue(!!title);
   });
 
   test('Title is not shown', function() {
-    const title = basePage.$$('#title');
+    const title = basePage.shadowRoot.querySelector('#title');
     assertFalse(!!title);
   });
 
   test('Message icon is shown', function() {
     basePage.messageIcon = 'warning';
     flush();
-    const messageIcon = basePage.$$('iron-icon');
+    const messageIcon = basePage.shadowRoot.querySelector('iron-icon');
     assertTrue(!!messageIcon);
     assertFalse(messageIcon.hidden);
   });
 
   test('Message icon is not shown', function() {
-    const messageIcon = basePage.$$('iron-icon');
+    const messageIcon = basePage.shadowRoot.querySelector('iron-icon');
     assertTrue(!!messageIcon);
     assertTrue(messageIcon.hidden);
   });
diff --git a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/button_bar_test.js b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/button_bar_test.js
index a50e88d..6ab3e49 100644
--- a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/button_bar_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/button_bar_test.js
@@ -61,23 +61,29 @@
 
   test('individual buttons appear if enabled', function() {
     setStateForAllButtons(ButtonState.ENABLED);
-    assertTrue(isButtonShownAndEnabled(buttonBar.$$('#backward')));
-    assertTrue(isButtonShownAndEnabled(buttonBar.$$('#cancel')));
-    assertTrue(isButtonShownAndEnabled(buttonBar.$$('#forward')));
+    assertTrue(isButtonShownAndEnabled(
+        buttonBar.shadowRoot.querySelector('#backward')));
+    assertTrue(
+        isButtonShownAndEnabled(buttonBar.shadowRoot.querySelector('#cancel')));
+    assertTrue(isButtonShownAndEnabled(
+        buttonBar.shadowRoot.querySelector('#forward')));
   });
 
   test('individual buttons appear but are diabled', function() {
     setStateForAllButtons(ButtonState.DISABLED);
-    assertTrue(isButtonShownAndDisabled(buttonBar.$$('#backward')));
-    assertTrue(isButtonShownAndDisabled(buttonBar.$$('#cancel')));
-    assertTrue(isButtonShownAndDisabled(buttonBar.$$('#forward')));
+    assertTrue(isButtonShownAndDisabled(
+        buttonBar.shadowRoot.querySelector('#backward')));
+    assertTrue(isButtonShownAndDisabled(
+        buttonBar.shadowRoot.querySelector('#cancel')));
+    assertTrue(isButtonShownAndDisabled(
+        buttonBar.shadowRoot.querySelector('#forward')));
   });
 
   test('individual buttons are hidden', function() {
     setStateForAllButtons(ButtonState.HIDDEN);
-    assertTrue(isButtonHidden(buttonBar.$$('#backward')));
-    assertTrue(isButtonHidden(buttonBar.$$('#cancel')));
-    assertTrue(isButtonHidden(buttonBar.$$('#forward')));
+    assertTrue(isButtonHidden(buttonBar.shadowRoot.querySelector('#backward')));
+    assertTrue(isButtonHidden(buttonBar.shadowRoot.querySelector('#cancel')));
+    assertTrue(isButtonHidden(buttonBar.shadowRoot.querySelector('#forward')));
   });
 
   test('default focus is on last button if all are enabled', function() {
@@ -86,7 +92,9 @@
 
     flush();
 
-    assertEquals(buttonBar.shadowRoot.activeElement, buttonBar.$$('#forward'));
+    assertEquals(
+        buttonBar.shadowRoot.activeElement,
+        buttonBar.shadowRoot.querySelector('#forward'));
   });
 
   test('default focus is on first button if rest are hidden', function() {
@@ -99,7 +107,9 @@
 
     flush();
 
-    assertEquals(buttonBar.shadowRoot.activeElement, buttonBar.$$('#backward'));
+    assertEquals(
+        buttonBar.shadowRoot.activeElement,
+        buttonBar.shadowRoot.querySelector('#backward'));
   });
 
   test(
@@ -115,6 +125,7 @@
         flush();
 
         assertEquals(
-            buttonBar.shadowRoot.activeElement, buttonBar.$$('#backward'));
+            buttonBar.shadowRoot.activeElement,
+            buttonBar.shadowRoot.querySelector('#backward'));
       });
 });
diff --git a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/cellular_setup_test.js b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/cellular_setup_test.js
index 0a318a26..dedd143 100644
--- a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/cellular_setup_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/cellular_setup_test.js
@@ -55,16 +55,16 @@
   test('Show pSim flow ui', async function() {
     init();
     await flushAsync();
-    let eSimFlow = cellularSetupPage.$$('esim-flow-ui');
-    let pSimFlow = cellularSetupPage.$$('psim-flow-ui');
+    let eSimFlow = cellularSetupPage.shadowRoot.querySelector('esim-flow-ui');
+    let pSimFlow = cellularSetupPage.shadowRoot.querySelector('psim-flow-ui');
 
     assertTrue(!!eSimFlow);
     assertFalse(!!pSimFlow);
 
     cellularSetupPage.currentPageName = CellularSetupPageName.PSIM_FLOW_UI;
     await flushAsync();
-    eSimFlow = cellularSetupPage.$$('esim-flow-ui');
-    pSimFlow = cellularSetupPage.$$('psim-flow-ui');
+    eSimFlow = cellularSetupPage.shadowRoot.querySelector('esim-flow-ui');
+    pSimFlow = cellularSetupPage.shadowRoot.querySelector('psim-flow-ui');
 
     assertFalse(!!eSimFlow);
     assertTrue(!!pSimFlow);
@@ -73,8 +73,8 @@
   test('Show eSIM flow ui', async function() {
     init();
     await flushAsync();
-    const eSimFlow = cellularSetupPage.$$('esim-flow-ui');
-    const pSimFlow = cellularSetupPage.$$('psim-flow-ui');
+    const eSimFlow = cellularSetupPage.shadowRoot.querySelector('esim-flow-ui');
+    const pSimFlow = cellularSetupPage.shadowRoot.querySelector('psim-flow-ui');
 
     // By default eSIM flow is always shown
     assertTrue(!!eSimFlow);
diff --git a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/confirmation_code_page_legacy_test.js b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/confirmation_code_page_legacy_test.js
index 550d01d4..8c636c8 100644
--- a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/confirmation_code_page_legacy_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/confirmation_code_page_legacy_test.js
@@ -33,7 +33,8 @@
         'forward-navigation-requested', () => {
           eventFired = true;
         });
-    const input = confirmationCodePageLegacy.$$('#confirmationCode');
+    const input = confirmationCodePageLegacy.shadowRoot.querySelector(
+        '#confirmationCode');
     input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
 
     await flushAsync();
diff --git a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/confirmation_code_page_test.js b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/confirmation_code_page_test.js
index fcb4df1..b2ae3566 100644
--- a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/confirmation_code_page_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/confirmation_code_page_test.js
@@ -33,7 +33,8 @@
         'forward-navigation-requested', () => {
           eventFired = true;
         });
-    const input = confirmationCodePage.$$('#confirmationCode');
+    const input =
+        confirmationCodePage.shadowRoot.querySelector('#confirmationCode');
     input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
 
     await flushAsync();
diff --git a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/esim_flow_ui_legacy_test.js b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/esim_flow_ui_legacy_test.js
index a661e49..0348122 100644
--- a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/esim_flow_ui_legacy_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/esim_flow_ui_legacy_test.js
@@ -127,12 +127,16 @@
     setSmdsSupportEnabled(false);
     flush();
 
-    ironPages = eSimPage.$$('iron-pages');
-    profileLoadingPage = eSimPage.$$('#profileLoadingPage');
-    profileDiscoveryPageLegacy = eSimPage.$$('#profileDiscoveryPageLegacy');
-    activationCodePage = eSimPage.$$('#activationCodePage');
-    confirmationCodePageLegacy = eSimPage.$$('#confirmationCodePageLegacy');
-    finalPage = eSimPage.$$('#finalPage');
+    ironPages = eSimPage.shadowRoot.querySelector('iron-pages');
+    profileLoadingPage =
+        eSimPage.shadowRoot.querySelector('#profileLoadingPage');
+    profileDiscoveryPageLegacy =
+        eSimPage.shadowRoot.querySelector('#profileDiscoveryPageLegacy');
+    activationCodePage =
+        eSimPage.shadowRoot.querySelector('#activationCodePage');
+    confirmationCodePageLegacy =
+        eSimPage.shadowRoot.querySelector('#confirmationCodePageLegacy');
+    finalPage = eSimPage.shadowRoot.querySelector('#finalPage');
 
     // Captures the function that is called every time the interval timer
     // timeouts.
@@ -200,7 +204,8 @@
 
   async function enterConfirmationCode(backButtonState) {
     const confirmationCodeInput =
-        confirmationCodePageLegacy.$$('#confirmationCode');
+        confirmationCodePageLegacy.shadowRoot.querySelector(
+            '#confirmationCode');
     confirmationCodeInput.value = 'CONFIRMATION_CODE';
     assertFalse(confirmationCodeInput.invalid);
 
@@ -216,7 +221,8 @@
 
   async function assertFinalPageAndPressDoneButton(shouldBeShowingError) {
     assertSelectedPage(ESimPageName.FINAL, finalPage);
-    assertEquals(!!finalPage.$$('.error'), shouldBeShowingError);
+    assertEquals(
+        !!finalPage.shadowRoot.querySelector('.error'), shouldBeShowingError);
     assertEquals(ButtonState.ENABLED, eSimPage.buttonState.forward);
     assertEquals(ButtonState.HIDDEN, eSimPage.buttonState.backward);
     assertEquals(ButtonState.HIDDEN, eSimPage.buttonState.cancel);
@@ -270,7 +276,9 @@
       forwardButtonShouldBeEnabled, backButtonState) {
     if (!forwardButtonShouldBeEnabled) {
       // In the initial state, input should be cleared.
-      assertEquals(activationCodePage.$$('#activationCode').value, '');
+      assertEquals(
+          activationCodePage.shadowRoot.querySelector('#activationCode').value,
+          '');
     }
     assertSelectedPage(ESimPageName.ACTIVATION_CODE, activationCodePage);
     assertButtonState(forwardButtonShouldBeEnabled, backButtonState);
@@ -281,7 +289,10 @@
     if (!forwardButtonShouldBeEnabled) {
       // In the initial state, input should be cleared.
       assertEquals(
-          confirmationCodePageLegacy.$$('#confirmationCode').value, '');
+          confirmationCodePageLegacy.shadowRoot
+              .querySelector('#confirmationCode')
+              .value,
+          '');
     }
     assertSelectedPage(
         ESimPageName.CONFIRMATION_CODE_LEGACY, confirmationCodePageLegacy);
@@ -306,7 +317,8 @@
           /*forwardButtonShouldBeEnabled*/ false,
           /*backButtonState*/ ButtonState.HIDDEN);
       // Insert an activation code.
-      activationCodePage.$$('#activationCode').value = ACTIVATION_CODE_VALID;
+      activationCodePage.shadowRoot.querySelector('#activationCode').value =
+          ACTIVATION_CODE_VALID;
 
       // Forward button should now be enabled.
       assertActivationCodePage(
@@ -404,8 +416,8 @@
       assertConfirmationCodePageLegacy(
           /*forwardButtonShouldBeEnabled*/ false,
           /*backButtonState*/ ButtonState.ENABLED);
-      confirmationCodePageLegacy.$$('#confirmationCode').value =
-          'CONFIRMATION_CODE';
+      confirmationCodePageLegacy.shadowRoot.querySelector('#confirmationCode')
+          .value = 'CONFIRMATION_CODE';
 
       eSimPage.navigateBackward();
       await flushAsync();
@@ -415,7 +427,7 @@
           /*forwardButtonShouldBeEnabled*/ true,
           /*backButtonState*/ ButtonState.HIDDEN);
       assertEquals(
-          activationCodePage.$$('#activationCode').value,
+          activationCodePage.shadowRoot.querySelector('#activationCode').value,
           ACTIVATION_CODE_VALID);
 
       endFlowAndVerifyResult(
@@ -467,7 +479,8 @@
       assertFocusDefaultButtonEventFired();
 
       // Insert an activation code.
-      activationCodePage.$$('#activationCode').value = ACTIVATION_CODE_VALID;
+      activationCodePage.shadowRoot.querySelector('#activationCode').value =
+          ACTIVATION_CODE_VALID;
       assertFalse(focusDefaultButtonEventFired);
 
       assertActivationCodePage(
@@ -512,8 +525,9 @@
                 /*forwardButtonShouldBeEnabled*/ false,
                 /*backButtonState*/ ButtonState.ENABLED);
             assertFocusDefaultButtonEventFired();
-            confirmationCodePageLegacy.$$('#confirmationCode').value =
-                'CONFIRMATION_CODE';
+            confirmationCodePageLegacy.shadowRoot
+                .querySelector('#confirmationCode')
+                .value = 'CONFIRMATION_CODE';
             assertFalse(focusDefaultButtonEventFired);
 
             // Simulate pressing 'Backward'.
@@ -525,7 +539,8 @@
                 /*backButtonState*/ ButtonState.ENABLED);
             assertFocusDefaultButtonEventFired();
             assertEquals(
-                activationCodePage.$$('#activationCode').value,
+                activationCodePage.shadowRoot.querySelector('#activationCode')
+                    .value,
                 ACTIVATION_CODE_VALID);
 
             eSimPage.navigateBackward();
@@ -543,7 +558,8 @@
 
     async function selectProfile() {
       // Select the first profile on the list.
-      const profileList = profileDiscoveryPageLegacy.$$('#profileList');
+      const profileList =
+          profileDiscoveryPageLegacy.shadowRoot.querySelector('#profileList');
       profileList.selectItem(profileList.items[0]);
       flush();
 
@@ -623,8 +639,9 @@
             assertConfirmationCodePageLegacy(
                 /*forwardButtonShouldBeEnabled*/ false,
                 /*backButtonState*/ ButtonState.ENABLED);
-            confirmationCodePageLegacy.$$('#confirmationCode').value =
-                'CONFIRMATION_CODE';
+            confirmationCodePageLegacy.shadowRoot
+                .querySelector('#confirmationCode')
+                .value = 'CONFIRMATION_CODE';
 
             eSimPage.navigateBackward();
             await flushAsync();
diff --git a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/esim_flow_ui_test.js b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/esim_flow_ui_test.js
index 22bda02..eefefb6 100644
--- a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/esim_flow_ui_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/esim_flow_ui_test.js
@@ -120,13 +120,18 @@
     document.body.appendChild(eSimPage);
     flush();
 
-    ironPages = eSimPage.$$('iron-pages');
-    profileLoadingPage = eSimPage.$$('#profileLoadingPage');
-    profileDiscoveryConsentPage = eSimPage.$$('#profileDiscoveryConsentPage');
-    profileDiscoveryPage = eSimPage.$$('#profileDiscoveryPage');
-    activationCodePage = eSimPage.$$('#activationCodePage');
-    confirmationCodePage = eSimPage.$$('#confirmationCodePage');
-    finalPage = eSimPage.$$('#finalPage');
+    ironPages = eSimPage.shadowRoot.querySelector('iron-pages');
+    profileLoadingPage =
+        eSimPage.shadowRoot.querySelector('#profileLoadingPage');
+    profileDiscoveryConsentPage =
+        eSimPage.shadowRoot.querySelector('#profileDiscoveryConsentPage');
+    profileDiscoveryPage =
+        eSimPage.shadowRoot.querySelector('#profileDiscoveryPage');
+    activationCodePage =
+        eSimPage.shadowRoot.querySelector('#activationCodePage');
+    confirmationCodePage =
+        eSimPage.shadowRoot.querySelector('#confirmationCodePage');
+    finalPage = eSimPage.shadowRoot.querySelector('#finalPage');
 
     // Captures the function that is called every time the interval timer
     // timeouts.
@@ -183,7 +188,8 @@
   }
 
   async function enterConfirmationCode(backButtonState) {
-    const confirmationCodeInput = confirmationCodePage.$$('#confirmationCode');
+    const confirmationCodeInput =
+        confirmationCodePage.shadowRoot.querySelector('#confirmationCode');
     confirmationCodeInput.value = 'CONFIRMATION_CODE';
     assertFalse(confirmationCodeInput.invalid);
 
@@ -199,7 +205,8 @@
 
   async function assertFinalPageAndPressDoneButton(shouldBeShowingError) {
     assertSelectedPage(ESimPageName.FINAL, finalPage);
-    assertEquals(!!finalPage.$$('.error'), shouldBeShowingError);
+    assertEquals(
+        !!finalPage.shadowRoot.querySelector('.error'), shouldBeShowingError);
     assertEquals(ButtonState.ENABLED, eSimPage.buttonState.forward);
     assertEquals(ButtonState.HIDDEN, eSimPage.buttonState.backward);
     assertEquals(ButtonState.HIDDEN, eSimPage.buttonState.cancel);
@@ -254,7 +261,7 @@
 
     // When the user clicks the "manually" link, they opt out of profile
     // discovery.
-    profileDiscoveryConsentPage.$$('#shouldSkipDiscovery')
+    profileDiscoveryConsentPage.shadowRoot.querySelector('#shouldSkipDiscovery')
         .shadowRoot.querySelector('a')
         .click();
     await flushAsync();
@@ -282,7 +289,9 @@
       forwardButtonShouldBeEnabled, backButtonState) {
     if (!forwardButtonShouldBeEnabled) {
       // In the initial state, input should be cleared.
-      assertEquals(activationCodePage.$$('#activationCode').value, '');
+      assertEquals(
+          activationCodePage.shadowRoot.querySelector('#activationCode').value,
+          '');
     }
     assertSelectedPage(ESimPageName.ACTIVATION_CODE, activationCodePage);
     assertButtonState(forwardButtonShouldBeEnabled);
@@ -292,7 +301,10 @@
       forwardButtonShouldBeEnabled, backButtonState) {
     if (!forwardButtonShouldBeEnabled) {
       // In the initial state, input should be cleared.
-      assertEquals(confirmationCodePage.$$('#confirmationCode').value, '');
+      assertEquals(
+          confirmationCodePage.shadowRoot.querySelector('#confirmationCode')
+              .value,
+          '');
     }
     assertSelectedPage(ESimPageName.CONFIRMATION_CODE, confirmationCodePage);
     assertButtonState(forwardButtonShouldBeEnabled);
@@ -340,8 +352,8 @@
                 /*forwardButtonShouldBeEnabled*/ false,
                 /*backButtonState*/ ButtonState.HIDDEN);
             // Insert an activation code.
-            activationCodePage.$$('#activationCode').value =
-                ACTIVATION_CODE_VALID;
+            activationCodePage.shadowRoot.querySelector('#activationCode')
+                .value = ACTIVATION_CODE_VALID;
             // Forward button should now be enabled.
             assertActivationCodePage(
                 /*forwardButtonShouldBeEnabled*/ true,
@@ -439,8 +451,8 @@
             assertConfirmationCodePage(
                 /*forwardButtonShouldBeEnabled*/ false,
                 /*backButtonState*/ ButtonState.ENABLED);
-            confirmationCodePage.$$('#confirmationCode').value =
-                'CONFIRMATION_CODE';
+            confirmationCodePage.shadowRoot.querySelector('#confirmationCode')
+                .value = 'CONFIRMATION_CODE';
 
             eSimPage.navigateBackward();
             await flushAsync();
@@ -450,7 +462,8 @@
                 /*forwardButtonShouldBeEnabled*/ true,
                 /*backButtonState*/ ButtonState.HIDDEN);
             assertEquals(
-                activationCodePage.$$('#activationCode').value,
+                activationCodePage.shadowRoot.querySelector('#activationCode')
+                    .value,
                 ACTIVATION_CODE_VALID);
 
             endFlowAndVerifyResult(
@@ -508,7 +521,8 @@
       assertFocusDefaultButtonEventFired();
 
       // Insert an activation code.
-      activationCodePage.$$('#activationCode').value = ACTIVATION_CODE_VALID;
+      activationCodePage.shadowRoot.querySelector('#activationCode').value =
+          ACTIVATION_CODE_VALID;
       assertFalse(focusDefaultButtonEventFired);
 
       assertActivationCodePage(
@@ -517,7 +531,7 @@
     }
 
     function skipProfileList() {
-      profileDiscoveryPage.$$('#profileListMessage')
+      profileDiscoveryPage.shadowRoot.querySelector('#profileListMessage')
           .shadowRoot.querySelector('a')
           .click();
 
@@ -530,7 +544,8 @@
       assertFocusDefaultButtonEventFired();
 
       // Insert an activation code.
-      activationCodePage.$$('#activationCode').value = ACTIVATION_CODE_VALID;
+      activationCodePage.shadowRoot.querySelector('#activationCode').value =
+          ACTIVATION_CODE_VALID;
       assertFalse(focusDefaultButtonEventFired);
 
       assertActivationCodePage(
@@ -620,8 +635,8 @@
                 /*forwardButtonShouldBeEnabled*/ false,
                 /*backButtonState*/ ButtonState.ENABLED);
             assertFocusDefaultButtonEventFired();
-            confirmationCodePage.$$('#confirmationCode').value =
-                'CONFIRMATION_CODE';
+            confirmationCodePage.shadowRoot.querySelector('#confirmationCode')
+                .value = 'CONFIRMATION_CODE';
             assertFalse(focusDefaultButtonEventFired);
 
             // Simulate pressing 'Backward'.
@@ -633,7 +648,8 @@
                 /*backButtonState*/ ButtonState.ENABLED);
             assertFocusDefaultButtonEventFired();
             assertEquals(
-                activationCodePage.$$('#activationCode').value,
+                activationCodePage.shadowRoot.querySelector('#activationCode')
+                    .value,
                 ACTIVATION_CODE_VALID);
 
             eSimPage.navigateBackward();
@@ -651,7 +667,8 @@
 
     async function selectProfile() {
       // Select the first profile on the list.
-      const profileList = profileDiscoveryPage.$$('#profileList');
+      const profileList =
+          profileDiscoveryPage.shadowRoot.querySelector('#profileList');
       profileList.selectItem(profileList.items[0]);
       flush();
 
@@ -728,8 +745,8 @@
             assertConfirmationCodePage(
                 /*forwardButtonShouldBeEnabled*/ false,
                 /*backButtonState*/ ButtonState.ENABLED);
-            confirmationCodePage.$$('#confirmationCode').value =
-                'CONFIRMATION_CODE';
+            confirmationCodePage.shadowRoot.querySelector('#confirmationCode')
+                .value = 'CONFIRMATION_CODE';
 
             eSimPage.navigateBackward();
             await flushAsync();
diff --git a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/final_page_test.js b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/final_page_test.js
index aaeb9af7..83c2d7ec 100644
--- a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/final_page_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/final_page_test.js
@@ -20,7 +20,7 @@
   });
 
   test('Base test', function() {
-    const basePage = finalPage.$$('base-page');
+    const basePage = finalPage.shadowRoot.querySelector('base-page');
     assertTrue(!!basePage);
   });
 });
diff --git a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/provisioning_page_test.js b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/provisioning_page_test.js
index a64850f..8a64ec8 100644
--- a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/provisioning_page_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/provisioning_page_test.js
@@ -21,7 +21,7 @@
   });
 
   test('Base test', function() {
-    const basePage = provisioningPage.$$('base-page');
+    const basePage = provisioningPage.shadowRoot.querySelector('base-page');
     assertTrue(!!basePage);
   });
 });
diff --git a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/psim_flow_ui_test.js b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/psim_flow_ui_test.js
index 4f98d6d..4dac726 100644
--- a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/psim_flow_ui_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/psim_flow_ui_test.js
@@ -98,7 +98,8 @@
     cellularActivationDelegate =
         cellularSetupRemote.getLastActivationDelegate();
 
-    const provisioningPage = pSimPage.$$('#provisioningPage');
+    const provisioningPage =
+        pSimPage.shadowRoot.querySelector('#provisioningPage');
     assertTrue(!!provisioningPage);
     assertFalse(
         pSimPage.selectedPSimPageName_ === PSimPageName.provisioningPage);
@@ -222,7 +223,8 @@
     cellularActivationDelegate =
         cellularSetupRemote.getLastActivationDelegate();
 
-    const provisioningPage = pSimPage.$$('#provisioningPage');
+    const provisioningPage =
+        pSimPage.shadowRoot.querySelector('#provisioningPage');
     assertTrue(!!provisioningPage);
     assertFalse(
         pSimPage.selectedPSimPageName_ === PSimPageName.provisioningPage);
@@ -235,7 +237,8 @@
   });
 
   test('Portal error metric logged', () => {
-    const provisioningPage = pSimPage.$$('#provisioningPage');
+    const provisioningPage =
+        pSimPage.shadowRoot.querySelector('#provisioningPage');
     provisioningPage.fire('carrier-portal-result', false);
 
     endFlowAndVerifyResult(PSimSetupFlowResult.CANCELLED_PORTAL_ERROR);
diff --git a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/setup_loading_page_test.js b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/setup_loading_page_test.js
index f7f5f2e..f210c44 100644
--- a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/setup_loading_page_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/setup_loading_page_test.js
@@ -18,19 +18,23 @@
     document.body.appendChild(setupLoadingPage);
     flush();
 
-    basePage = setupLoadingPage.$$('base-page');
+    basePage = setupLoadingPage.shadowRoot.querySelector('base-page');
     assertTrue(!!basePage);
   });
 
   test('Loading animation and error graphic shown correctly', function() {
     setupLoadingPage.isSimDetectError = false;
     flush();
-    assertTrue(!!setupLoadingPage.$$('#animationContainer'));
-    assertTrue(setupLoadingPage.$$('#simDetectError').hidden);
+    assertTrue(
+        !!setupLoadingPage.shadowRoot.querySelector('#animationContainer'));
+    assertTrue(
+        setupLoadingPage.shadowRoot.querySelector('#simDetectError').hidden);
 
     setupLoadingPage.isSimDetectError = true;
     flush();
-    assertFalse(!!setupLoadingPage.$$('#animationContainer'));
-    assertFalse(setupLoadingPage.$$('#simDetectError').hidden);
+    assertFalse(
+        !!setupLoadingPage.shadowRoot.querySelector('#animationContainer'));
+    assertFalse(
+        setupLoadingPage.shadowRoot.querySelector('#simDetectError').hidden);
   });
 });
diff --git a/chrome/updater/configurator.cc b/chrome/updater/configurator.cc
index 0a6e645..fe16252 100644
--- a/chrome/updater/configurator.cc
+++ b/chrome/updater/configurator.cc
@@ -231,4 +231,8 @@
   return updater::GetCrxDiffCacheDirectory(GetUpdaterScope());
 }
 
+bool Configurator::IsConnectionMetered() const {
+  return false;
+}
+
 }  // namespace updater
diff --git a/chrome/updater/configurator.h b/chrome/updater/configurator.h
index 1c0cd33..53ff874 100644
--- a/chrome/updater/configurator.h
+++ b/chrome/updater/configurator.h
@@ -80,6 +80,7 @@
   std::optional<bool> IsMachineExternallyManaged() const override;
   update_client::UpdaterStateProvider GetUpdaterStateProvider() const override;
   std::optional<base::FilePath> GetCrxCachePath() const override;
+  bool IsConnectionMetered() const override;
 
   scoped_refptr<PersistedData> GetUpdaterPersistedData() const;
   virtual GURL CrashUploadURL() const;
diff --git a/chrome/updater/test/integration_tests_win.cc b/chrome/updater/test/integration_tests_win.cc
index c4c96c6..8a457de2 100644
--- a/chrome/updater/test/integration_tests_win.cc
+++ b/chrome/updater/test/integration_tests_win.cc
@@ -181,35 +181,15 @@
 void ExpectUpdateRegKeyClean(UpdaterScope scope) {
   const HKEY root = UpdaterScopeToHKeyRoot(scope);
 
-  if (!RegKeyExists(root, UPDATER_KEY)) {
-    return;
-  }
-
-  for (base::win::RegistryValueIterator updater_value_iter(root, UPDATER_KEY,
-                                                           KEY_WOW64_32KEY);
-       updater_value_iter.Valid(); ++updater_value_iter) {
-    EXPECT_TRUE(
-        base::Contains(kRegValuesLastInstaller, updater_value_iter.Name()))
-        << updater_value_iter.Name();
-  }
-
-  base::win::RegistryKeyIterator updater_key_iter(root, UPDATER_KEY,
-                                                  KEY_WOW64_32KEY);
   if (IsSystemInstall(scope)) {
-    // App activity bits are written to HKCU only.
-    EXPECT_EQ(updater_key_iter.SubkeyCount(), 0u);
+    EXPECT_EQ(RegKeyExists(root, CLIENT_STATE_KEY), false);
     return;
   }
 
-  if (updater_key_iter.SubkeyCount() == 0) {
+  // `ClientState` may exist with lastrun values for user installs.
+  if (!RegKeyExists(root, CLIENT_STATE_KEY)) {
     return;
   }
-
-  // `ClientState` is the only allowed sub-key of `UPDATER_KEY` for user
-  // installs.
-  EXPECT_EQ(updater_key_iter.SubkeyCount(), 1u);
-  EXPECT_STREQ(updater_key_iter.Name(), L"ClientState");
-
   EXPECT_THAT(base::win::RegKey(root, CLIENT_STATE_KEY, Wow6432(KEY_READ))
                   .GetValueCount(),
               base::test::ValueIs(0u));
@@ -297,7 +277,7 @@
           EXPECT_FALSE(RegKeyExists(HKEY_LOCAL_MACHINE, key));
         }
       }
-      EXPECT_FALSE(RegKeyExists(root, CLIENTS_KEY));
+      EXPECT_FALSE(RegKeyExists(root, CLIENT_STATE_KEY));
       ExpectUpdateRegKeyClean(scope);
 
       if (!IsSystemInstall(scope)) {
diff --git a/chrome/updater/win/setup/uninstall.cc b/chrome/updater/win/setup/uninstall.cc
index 2cbc16a..668672f7 100644
--- a/chrome/updater/win/setup/uninstall.cc
+++ b/chrome/updater/win/setup/uninstall.cc
@@ -95,47 +95,16 @@
   }
 }
 
-void DeleteUpdaterKey(UpdaterScope scope) {
-  const HKEY root = UpdaterScopeToHKeyRoot(scope);
-
-  // Delete all the sub keys of `UPDATER_KEY`.
-  std::vector<std::wstring> sub_keys;
-  for (base::win::RegistryKeyIterator updater_key_iter(root, UPDATER_KEY,
-                                                       KEY_WOW64_32KEY);
-       updater_key_iter.Valid(); ++updater_key_iter) {
-    sub_keys.push_back(updater_key_iter.Name());
-  }
-
-  for (const auto& sub_key : sub_keys) {
-    const std::wstring subkey_path =
-        base::StrCat({UPDATER_KEY, L"\\", sub_key});
-    installer::DeleteRegistryKey(root, subkey_path, KEY_WOW64_32KEY);
-  }
-
-  // Delete all the values of `UPDATER_KEY`, except the `LastInstaller*` values.
-  std::vector<std::wstring> values;
-  for (base::win::RegistryValueIterator updater_value_iter(root, UPDATER_KEY,
-                                                           KEY_WOW64_32KEY);
-       updater_value_iter.Valid(); ++updater_value_iter) {
-    if (!base::Contains(kRegValuesLastInstaller, updater_value_iter.Name())) {
-      values.push_back(updater_value_iter.Name());
-    }
-  }
-  for (const auto& value : values) {
-    installer::DeleteRegistryValue(root, UPDATER_KEY, KEY_WOW64_32KEY, value);
-  }
-
-  // Finally, delete `UPDATER_KEY` if it is empty.
-  base::win::RegKey updater_key;
-  if (updater_key.Open(root, UPDATER_KEY, Wow6432(KEY_QUERY_VALUE)) ==
-          ERROR_SUCCESS &&
-      updater_key.GetValueCount().value_or(1) == 0) {
-    updater_key.DeleteKey(L"", base::win::RegKey::RecursiveDelete(false));
+void DeleteClientStateKey(UpdaterScope scope) {
+  base::win::RegKey client_state;
+  if (client_state.Open(UpdaterScopeToHKeyRoot(scope), CLIENT_STATE_KEY,
+                        Wow6432(KEY_QUERY_VALUE)) == ERROR_SUCCESS) {
+    client_state.DeleteKey(L"", base::win::RegKey::RecursiveDelete(true));
   }
 }
 
 void DeleteGoogleUpdateFilesAndKeys(UpdaterScope scope) {
-  DeleteUpdaterKey(scope);
+  DeleteClientStateKey(scope);
 
   const std::optional<base::FilePath> target_path =
       GetGoogleUpdateExePath(scope);
@@ -185,7 +154,7 @@
 
 // Reverses the changes made by setup. This is a best effort uninstall:
 // 1. Deletes the scheduled task.
-// 2. Deletes the Clients and ClientState keys.
+// 2. Deletes the ClientState key.
 // 3. Runs the uninstall script in the install directory of the updater.
 // The execution of this function and the script race each other but the script
 // loops and waits in between iterations trying to delete the install directory.
diff --git a/chromeos/ash/components/assistant/OWNERS b/chromeos/ash/components/assistant/OWNERS
index fbebcec..0dd0eb7 100644
--- a/chromeos/ash/components/assistant/OWNERS
+++ b/chromeos/ash/components/assistant/OWNERS
@@ -2,6 +2,7 @@
 # unless there is a specific reason for it.
 assistive-code-review@google.com
 
+angelaxiao@chromium.org #{LAST_RESORT_SUGGESTION}
 esum@google.com #{LAST_RESORT_SUGGESTION}
 wutao@chromium.org #{LAST_RESORT_SUGGESTION}
 xiaohuic@chromium.org #{LAST_RESORT_SUGGESTION}
diff --git a/chromeos/chromeos_strings.grd b/chromeos/chromeos_strings.grd
index 573ed32..bcedd23 100644
--- a/chromeos/chromeos_strings.grd
+++ b/chromeos/chromeos_strings.grd
@@ -4616,13 +4616,13 @@
           <ph name="ATTACH">Attach</ph> (<ph name="IA">IA</ph>)
         </message>
         <message name="IDS_SETTINGS_APN_A11Y_DEFAULT_AND_ATTACH_APN" desc="Accessibility text for when an APN is both a default and an attach APN">
-          APN is type default and attach.
+          APN is type default and <ph name="ATTACH">attach</ph>.
         </message>
         <message name="IDS_SETTINGS_APN_A11Y_DEFAULT_APN_ONLY" desc="Accessibility text for when an APN is only a default APN">
           APN is type default.
         </message>
         <message name="IDS_SETTINGS_APN_A11Y_ATTACH_APN_ONLY" desc="Accessibility text for when an APN is only an attach APN">
-          APN is type attach.
+          APN is type <ph name="ATTACH">attach</ph>.
         </message>
         <message name="IDS_SETTINGS_APN_DIALOG_DEFAULT_APN_REQUIRED" desc="Error message that is shown whenever a default APN type is required">
           A default APN is required
@@ -4634,7 +4634,7 @@
           Automatically detected
         </message>
         <message name="IDS_SETTINGS_APN_WARNING_PROMPT_FOR_DISABLE_REMOVE" desc="Warning Prompt describing why the APN can't be disabled or removed.">
-          Can't disable or remove this APN. Make sure enabled attach APNs are disabled or removed.
+          Can't disable or remove this APN. Make sure enabled <ph name="ATTACH">attach</ph> APNs are disabled or removed.
         </message>
         <message name="IDS_SETTINGS_APN_WARNING_PROMPT_FOR_ENABLE" desc="Warning Prompt describing why the APN can't be enabled.">
           Can’t enable this APN. Make sure a default APN is added.
diff --git a/chromeos/chromeos_strings_grd/IDS_SETTINGS_APN_A11Y_ATTACH_APN_ONLY.png.sha1 b/chromeos/chromeos_strings_grd/IDS_SETTINGS_APN_A11Y_ATTACH_APN_ONLY.png.sha1
index f0a27785..dc9c4704 100644
--- a/chromeos/chromeos_strings_grd/IDS_SETTINGS_APN_A11Y_ATTACH_APN_ONLY.png.sha1
+++ b/chromeos/chromeos_strings_grd/IDS_SETTINGS_APN_A11Y_ATTACH_APN_ONLY.png.sha1
@@ -1 +1 @@
-580ad7bfd1ac9c3c261fdd02f07298d3e924dc8b
\ No newline at end of file
+c5730e2c8e36799256b4d2ba9d39e454f2d04e91
\ No newline at end of file
diff --git a/chromeos/chromeos_strings_grd/IDS_SETTINGS_APN_A11Y_DEFAULT_AND_ATTACH_APN.png.sha1 b/chromeos/chromeos_strings_grd/IDS_SETTINGS_APN_A11Y_DEFAULT_AND_ATTACH_APN.png.sha1
index 0b4dd31..dc9c4704 100644
--- a/chromeos/chromeos_strings_grd/IDS_SETTINGS_APN_A11Y_DEFAULT_AND_ATTACH_APN.png.sha1
+++ b/chromeos/chromeos_strings_grd/IDS_SETTINGS_APN_A11Y_DEFAULT_AND_ATTACH_APN.png.sha1
@@ -1 +1 @@
-77d95ecf7eed3bda97ceef5f94830e3a4b8b6194
\ No newline at end of file
+c5730e2c8e36799256b4d2ba9d39e454f2d04e91
\ No newline at end of file
diff --git a/chromeos/chromeos_strings_grd/IDS_SETTINGS_APN_WARNING_PROMPT_FOR_DISABLE_REMOVE.png.sha1 b/chromeos/chromeos_strings_grd/IDS_SETTINGS_APN_WARNING_PROMPT_FOR_DISABLE_REMOVE.png.sha1
index a8120fdd..b0f4a70 100644
--- a/chromeos/chromeos_strings_grd/IDS_SETTINGS_APN_WARNING_PROMPT_FOR_DISABLE_REMOVE.png.sha1
+++ b/chromeos/chromeos_strings_grd/IDS_SETTINGS_APN_WARNING_PROMPT_FOR_DISABLE_REMOVE.png.sha1
@@ -1 +1 @@
-b39ce41e2160c0b38aa30a0a25f8774dd0ae6f33
\ No newline at end of file
+b0a495b4cd1e9ef8e1443bf88e88ba3936965263
\ No newline at end of file
diff --git a/clank b/clank
index 73c5549..20d5657 160000
--- a/clank
+++ b/clank
@@ -1 +1 @@
-Subproject commit 73c554959a79725f3f2e1a6f9a902bd4fc2febc3
+Subproject commit 20d5657767c7a726b9144002e4021d087725a89e
diff --git a/components/component_updater/BUILD.gn b/components/component_updater/BUILD.gn
index 13f683b..d459f161 100644
--- a/components/component_updater/BUILD.gn
+++ b/components/component_updater/BUILD.gn
@@ -47,6 +47,7 @@
     "//components/prefs",
     "//components/update_client",
     "//components/version_info",
+    "//net",
     "//third_party/boringssl:boringssl",
     "//ui/base",
     "//url",
@@ -83,6 +84,7 @@
     "//components/crx_file",
     "//components/prefs:test_support",
     "//components/update_client:test_support",
+    "//net:test_support",
     "//services/service_manager/public/cpp",
     "//testing/gmock",
     "//testing/gtest",
diff --git a/components/component_updater/DEPS b/components/component_updater/DEPS
index 68c6559..8596a02 100644
--- a/components/component_updater/DEPS
+++ b/components/component_updater/DEPS
@@ -6,4 +6,5 @@
   "+services/service_manager/public",
   "+ui",
   "+url",
+  "+net",
 ]
diff --git a/components/component_updater/component_installer.cc b/components/component_updater/component_installer.cc
index fbc62f5..cf9f785 100644
--- a/components/component_updater/component_installer.cc
+++ b/components/component_updater/component_installer.cc
@@ -57,6 +57,10 @@
   return true;
 }
 
+bool ComponentInstallerPolicy::AllowUpdatesOnMeteredConnections() const {
+  return true;
+}
+
 ComponentInstaller::RegistrationInfo::RegistrationInfo()
     : version(kNullVersion) {}
 
@@ -529,7 +533,8 @@
                installer_policy_->GetInstallerAttributes(), action_handler_,
                this, installer_policy_->RequiresNetworkEncryption(),
                installer_policy_->SupportsGroupPolicyEnabledComponentUpdates(),
-               installer_policy_->AllowCachedCopies()))) {
+               installer_policy_->AllowCachedCopies(),
+               installer_policy_->AllowUpdatesOnMeteredConnections()))) {
     VLOG(0) << "Component registration failed for "
             << installer_policy_->GetName();
     if (!callback.is_null()) {
diff --git a/components/component_updater/component_installer.h b/components/component_updater/component_installer.h
index fe597860..992c017 100644
--- a/components/component_updater/component_installer.h
+++ b/components/component_updater/component_installer.h
@@ -122,6 +122,12 @@
   // storage overhead is valued higher than the cost of performing a full update
   // at the expected cadence, disabling cached copies is a reasonable choice.
   virtual bool AllowCachedCopies() const;
+
+  // Returns true if the component should not update over metered connections.
+  // Defaults to |true|. This only controls whether updates are accepted: if the
+  // network type changes from unmetered to metered during a download, there is
+  // no guarantee that the transfer will be suspended or cancelled.
+  virtual bool AllowUpdatesOnMeteredConnections() const;
 };
 
 // Defines the installer for Chrome components. The behavior of this class is
diff --git a/components/component_updater/component_installer_unittest.cc b/components/component_updater/component_installer_unittest.cc
index 7fae408a..eb140b0b 100644
--- a/components/component_updater/component_installer_unittest.cc
+++ b/components/component_updater/component_installer_unittest.cc
@@ -8,6 +8,7 @@
 #include <utility>
 #include <vector>
 
+#include "base/barrier_closure.h"
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
 #include "base/functional/bind.h"
@@ -20,6 +21,7 @@
 #include "base/sequence_checker.h"
 #include "base/strings/strcat.h"
 #include "base/strings/stringprintf.h"
+#include "base/task/bind_post_task.h"
 #include "base/task/sequenced_task_runner.h"
 #include "base/test/bind.h"
 #include "base/test/scoped_path_override.h"
@@ -131,8 +133,10 @@
                               const base::FilePath& install_dir,
                               base::Value::Dict manifest)>;
   explicit MockInstallerPolicy(
-      ComponentReadyCallback component_ready_cb = ComponentReadyCallback())
-      : component_ready_cb_(std::move(component_ready_cb)) {}
+      ComponentReadyCallback component_ready_cb = ComponentReadyCallback(),
+      base::RepeatingClosure uninstall_cb = base::DoNothing())
+      : component_ready_cb_(std::move(component_ready_cb)),
+        uninstall_cb_(uninstall_cb) {}
   ~MockInstallerPolicy() override = default;
 
   bool VerifyInstallation(const base::Value::Dict& manifest,
@@ -152,7 +156,7 @@
     return update_client::CrxInstaller::Result(0);
   }
 
-  void OnCustomUninstall() override {}
+  void OnCustomUninstall() override { uninstall_cb_.Run(); }
 
   void ComponentReady(const base::Version& version,
                       const base::FilePath& install_dir,
@@ -184,6 +188,7 @@
   }
 
   ComponentReadyCallback component_ready_cb_;
+  base::RepeatingClosure uninstall_cb_;
 };
 
 class MockUpdateScheduler : public UpdateScheduler {
@@ -297,8 +302,9 @@
   base::FilePath component_dir =
       base_dir.AppendASCII(name).AppendASCII(version);
 
-  if (!base::CreateDirectory(component_dir))
+  if (!base::CreateDirectory(component_dir)) {
     return absl::nullopt;
+  }
 
   if (!base::WriteFile(component_dir.AppendASCII("manifest.json"),
                        base::StringPrintf(R"({
@@ -307,8 +313,9 @@
         "min_env_version": "%s"
     })",
                                           name.c_str(), version.c_str(),
-                                          min_env_version.c_str())))
+                                          min_env_version.c_str()))) {
     return absl::nullopt;
+  }
 
   return absl::make_optional(component_dir);
 }
@@ -317,32 +324,19 @@
 // and its component policy, through the instance of the CrxComponent, to the
 // component updater service.
 TEST_F(ComponentInstallerTest, RegisterComponent) {
-  class LoopHandler {
-   public:
-    LoopHandler(int max_cnt, base::OnceClosure quit_closure)
-        : max_cnt_(max_cnt), quit_closure_(std::move(quit_closure)) {}
-
-    void OnUpdate(const std::vector<std::string>& ids,
-                  const UpdateClient::CrxDataCallback& crx_data_callback) {
-      static int cnt = 0;
-      ++cnt;
-      if (cnt >= max_cnt_)
-        std::move(quit_closure_).Run();
-    }
-
-   private:
-    const int max_cnt_;
-    base::OnceClosure quit_closure_;
-  };
-
   base::ScopedPathOverride scoped_path_override(DIR_COMPONENT_USER);
 
   const std::string id("jebgalgnebhfojomionfpkfelancnnkf");
 
   // Quit after one update check has been fired.
-  LoopHandler loop_handler(1, quit_closure());
+  base::RepeatingClosure barrier_callback =
+      base::BarrierClosure(1, quit_closure());
   EXPECT_CALL(update_client(), DoUpdate(_, _))
-      .WillRepeatedly(Invoke(&loop_handler, &LoopHandler::OnUpdate));
+      .WillRepeatedly(
+          [&](const std::vector<std::string>& ids,
+              const UpdateClient::CrxDataCallback& crx_data_callback) {
+            barrier_callback.Run();
+          });
 
   EXPECT_CALL(update_client(), GetCrxUpdateState(id, _)).Times(1);
   EXPECT_CALL(update_client(), Stop()).Times(1);
@@ -590,4 +584,40 @@
   ASSERT_EQ(registration_info->version, base::Version("7.0.0.0"));
 }
 
+TEST_F(ComponentInstallerTest, Uninstall) {
+  base::RunLoop run_loop;
+  auto installer = base::MakeRefCounted<ComponentInstaller>(
+      std::make_unique<MockInstallerPolicy>(
+          MockInstallerPolicy::ComponentReadyCallback(),
+          base::BindPostTaskToCurrentDefault(run_loop.QuitClosure())));
+
+  Unpack(
+      update_client::GetTestFilePath("jebgalgnebhfojomionfpkfelancnnkf.crx"));
+
+  const auto unpack_path = result().unpack_path;
+  EXPECT_TRUE(base::DirectoryExists(unpack_path));
+  EXPECT_EQ(update_client::jebg_public_key, result().public_key);
+
+  base::ScopedPathOverride scoped_path_override(DIR_COMPONENT_USER);
+  base::FilePath base_dir;
+  EXPECT_TRUE(base::PathService::Get(DIR_COMPONENT_USER, &base_dir));
+  base_dir = base_dir.Append(relative_install_dir);
+  EXPECT_TRUE(base::CreateDirectory(base_dir));
+
+  installer->Register(
+      component_updater(), base::BindLambdaForTesting([&]() {
+        installer->Install(
+            unpack_path, update_client::jebg_public_key, nullptr,
+            base::DoNothing(),
+            base::BindLambdaForTesting(
+                [&](const update_client::CrxInstaller::Result& result) {
+                  EXPECT_EQ(0, result.error);
+                  installer->Uninstall();
+                }));
+      }));
+  run_loop.Run();
+
+  EXPECT_FALSE(base::PathExists(base_dir));
+}
+
 }  // namespace component_updater
diff --git a/components/component_updater/component_updater_service.cc b/components/component_updater/component_updater_service.cc
index ed21e9c..b54eff1 100644
--- a/components/component_updater/component_updater_service.cc
+++ b/components/component_updater/component_updater_service.cc
@@ -80,7 +80,8 @@
     scoped_refptr<update_client::CrxInstaller> installer,
     bool requires_network_encryption,
     bool supports_group_policy_enable_component_updates,
-    bool allow_cached_copies)
+    bool allow_cached_copies,
+    bool allow_updates_on_metered_connection)
     : app_id(app_id),
       name(name),
       public_key_hash(public_key_hash),
@@ -92,7 +93,9 @@
       requires_network_encryption(requires_network_encryption),
       supports_group_policy_enable_component_updates(
           supports_group_policy_enable_component_updates),
-      allow_cached_copies(allow_cached_copies) {}
+      allow_cached_copies(allow_cached_copies),
+      allow_updates_on_metered_connection(allow_updates_on_metered_connection) {
+}
 ComponentRegistration::ComponentRegistration(
     const ComponentRegistration& other) = default;
 ComponentRegistration& ComponentRegistration::operator=(
@@ -285,6 +288,8 @@
   crx.installer_attributes = component.installer_attributes;
   crx.requires_network_encryption = component.requires_network_encryption;
   crx.allow_cached_copies = component.allow_cached_copies;
+  crx.allow_updates_on_metered_connection =
+      component.allow_updates_on_metered_connection;
 
   crx.brand = brand_;
   crx.crx_format_requirement =
diff --git a/components/component_updater/component_updater_service.h b/components/component_updater/component_updater_service.h
index ef30796..6838a99 100644
--- a/components/component_updater/component_updater_service.h
+++ b/components/component_updater/component_updater_service.h
@@ -94,7 +94,8 @@
       scoped_refptr<update_client::CrxInstaller> installer,
       bool requires_network_encryption,
       bool supports_group_policy_enable_component_updates,
-      bool allow_cached_copies);
+      bool allow_cached_copies,
+      bool allow_updates_on_metered_connection);
   ComponentRegistration(const ComponentRegistration& other);
   ComponentRegistration& operator=(const ComponentRegistration& other);
   ComponentRegistration(ComponentRegistration&& other);
@@ -112,6 +113,7 @@
   bool requires_network_encryption;
   bool supports_group_policy_enable_component_updates;
   bool allow_cached_copies;
+  bool allow_updates_on_metered_connection;
 };
 
 // The component update service is in charge of installing or upgrading select
diff --git a/components/component_updater/component_updater_service_unittest.cc b/components/component_updater/component_updater_service_unittest.cc
index fbb78ccb..0a2bcbe 100644
--- a/components/component_updater/component_updater_service_unittest.cc
+++ b/components/component_updater/component_updater_service_unittest.cc
@@ -317,12 +317,23 @@
 
   std::vector<uint8_t> hash;
   hash.assign(std::begin(abag_hash), std::end(abag_hash));
-  ComponentRegistration component1(id1, {}, hash, base::Version("1.0"), {}, {},
-                                   nullptr, installer, false, true, true);
+  ComponentRegistration component1(
+      id1, /*name=*/{}, hash, base::Version("1.0"), /*fingerprint=*/{}, {},
+      /*action_handler=*/nullptr, installer,
+      /*requires_network_encryption=*/false,
+      /*supports_group_policy_enable_component_updates=*/true,
+      /*allow_cached_copies=*/true,
+      /*allow_updates_on_metered_connection=*/true);
 
   hash.assign(std::begin(jebg_hash), std::end(jebg_hash));
-  ComponentRegistration component2(id2, {}, hash, base::Version("0.9"), {}, {},
-                                   nullptr, installer, false, true, true);
+  ComponentRegistration component2(
+      id2, /*name=*/{}, hash, base::Version("0.9"),
+      /*fingerprint=*/{}, /*installer_attributes=*/{},
+      /*action_handler=*/nullptr, installer,
+      /*requires_network_encryption=*/false,
+      /*supports_group_policy_enable_component_updates=*/true,
+      /*allow_cached_copies=*/true,
+      /*allow_updates_on_metered_connection=*/true);
 
   // Quit after two update checks have fired.
   LoopHandler loop_handler(2, quit_closure());
@@ -381,18 +392,27 @@
     std::vector<uint8_t> hash;
     hash.assign(std::begin(jebg_hash), std::end(jebg_hash));
     EXPECT_TRUE(cus.RegisterComponent(ComponentRegistration(
-        "jebgalgnebhfojomionfpkfelancnnkf", {}, hash, base::Version("0.9"), {},
-        {}, nullptr, base::MakeRefCounted<MockInstaller>(), false, true,
-        true)));
+        "jebgalgnebhfojomionfpkfelancnnkf", /*name=*/{}, hash,
+        base::Version("0.9"), /*fingerprint=*/{}, /*installer_attributes=*/{},
+        /*action_handler=*/nullptr, base::MakeRefCounted<MockInstaller>(),
+        /*requires_network_encryption=*/false,
+        /*supports_group_policy_enable_component_updates=*/true,
+        /*allow_cached_copies=*/true,
+        /*allow_updates_on_metered_connection=*/true)));
   }
   {
     using update_client::abag_hash;
     std::vector<uint8_t> hash;
     hash.assign(std::begin(abag_hash), std::end(abag_hash));
     EXPECT_TRUE(cus.RegisterComponent(ComponentRegistration(
-        "abagagagagagagagagagagagagagagag", {}, hash, base::Version("0.9"), {},
-        {}, nullptr, base::MakeRefCounted<MockInstaller>(), false, true,
-        true)));
+        "abagagagagagagagagagagagagagagag", /*name=*/{}, hash,
+        base::Version("0.9"), /*fingerprint=*/{},
+        /*installer_attributes=*/{}, /*action_handler=*/nullptr,
+        base::MakeRefCounted<MockInstaller>(),
+        /*requires_network_encryption=*/false,
+        /*supports_group_policy_enable_component_updates=*/true,
+        /*allow_cached_copies=*/true,
+        /*allow_updates_on_metered_connection=*/true)));
   }
 
   OnDemandTester ondemand_tester;
@@ -430,8 +450,14 @@
   EXPECT_CALL(scheduler(), Stop()).Times(1);
 
   EXPECT_TRUE(component_updater().RegisterComponent(ComponentRegistration(
-      "jebgalgnebhfojomionfpkfelancnnkf", {}, hash, base::Version("0.9"), {},
-      {}, nullptr, base::MakeRefCounted<MockInstaller>(), false, true, true)));
+      "jebgalgnebhfojomionfpkfelancnnkf", /*name=*/{}, hash,
+      base::Version("0.9"), {},
+      /*installer_attributes=*/{}, /*action_handler=*/nullptr,
+      base::MakeRefCounted<MockInstaller>(),
+      /*requires_network_encryption=*/false,
+      /*supports_group_policy_enable_component_updates=*/true,
+      /*allow_cached_copies=*/true,
+      /*allow_updates_on_metered_connection=*/true)));
   component_updater().MaybeThrottle("jebgalgnebhfojomionfpkfelancnnkf",
                                     base::DoNothing());
 
diff --git a/components/component_updater/configurator_impl.cc b/components/component_updater/configurator_impl.cc
index 0e9fe9c..20612b65 100644
--- a/components/component_updater/configurator_impl.cc
+++ b/components/component_updater/configurator_impl.cc
@@ -23,6 +23,7 @@
 #include "components/update_client/protocol_handler.h"
 #include "components/update_client/utils.h"
 #include "components/version_info/version_info.h"
+#include "net/base/network_change_notifier.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "url/gurl.h"
 
@@ -51,8 +52,9 @@
 ConfiguratorImpl::~ConfiguratorImpl() = default;
 
 base::TimeDelta ConfiguratorImpl::InitialDelay() const {
-  if (!initial_delay_.is_zero())
+  if (!initial_delay_.is_zero()) {
     return initial_delay_;
+  }
   return fast_update_ ? base::Seconds(10) : base::Minutes(1);
 }
 
@@ -69,13 +71,15 @@
 }
 
 std::vector<GURL> ConfiguratorImpl::UpdateUrl() const {
-  if (url_source_override_.is_valid())
+  if (url_source_override_.is_valid()) {
     return {GURL(url_source_override_)};
+  }
 
   std::vector<GURL> urls{GURL(kUpdaterJSONDefaultUrl),
                          GURL(kUpdaterJSONFallbackUrl)};
-  if (require_encryption_)
+  if (require_encryption_) {
     update_client::RemoveUnsecureUrls(&urls);
+  }
 
   return urls;
 }
@@ -151,4 +155,9 @@
 #endif
 }
 
+bool ConfiguratorImpl::IsConnectionMetered() const {
+  return net::NetworkChangeNotifier::GetConnectionCost() ==
+         net::NetworkChangeNotifier::CONNECTION_COST_METERED;
+}
+
 }  // namespace component_updater
diff --git a/components/component_updater/configurator_impl.h b/components/component_updater/configurator_impl.h
index 9a31551..a7ea4da 100644
--- a/components/component_updater/configurator_impl.h
+++ b/components/component_updater/configurator_impl.h
@@ -99,6 +99,8 @@
 
   update_client::UpdaterStateProvider GetUpdaterStateProvider() const;
 
+  bool IsConnectionMetered() const;
+
  private:
   base::flat_map<std::string, std::string> extra_info_;
   const bool background_downloads_enabled_;
diff --git a/components/component_updater/configurator_impl_unittest.cc b/components/component_updater/configurator_impl_unittest.cc
index ad6a5b6c..dcaf61b 100644
--- a/components/component_updater/configurator_impl_unittest.cc
+++ b/components/component_updater/configurator_impl_unittest.cc
@@ -6,11 +6,14 @@
 
 #include "base/command_line.h"
 #include "base/test/scoped_command_line.h"
+#include "base/test/task_environment.h"
 #include "base/time/time.h"
 #include "components/component_updater/component_updater_command_line_config_policy.h"
 #include "components/component_updater/component_updater_switches.h"
 #include "components/component_updater/configurator_impl.h"
 #include "components/update_client/command_line_config_policy.h"
+#include "net/base/mock_network_change_notifier.h"
+#include "net/base/network_change_notifier.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace component_updater {
@@ -25,6 +28,9 @@
       const ComponentUpdaterConfiguratorImplTest&) = delete;
 
   ~ComponentUpdaterConfiguratorImplTest() override = default;
+
+ private:
+  base::test::TaskEnvironment environment_;
 };
 
 TEST_F(ComponentUpdaterConfiguratorImplTest, FastUpdate) {
@@ -32,19 +38,19 @@
   base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
   std::unique_ptr<ConfiguratorImpl> config = std::make_unique<ConfiguratorImpl>(
       ComponentUpdaterCommandLineConfigPolicy(&cmdline), false);
-  CHECK_EQ(base::Minutes(1), config->InitialDelay());
-  CHECK_EQ(base::Hours(5), config->NextCheckDelay());
-  CHECK_EQ(base::Minutes(30), config->OnDemandDelay());
-  CHECK_EQ(base::Minutes(15), config->UpdateDelay());
+  EXPECT_EQ(base::Minutes(1), config->InitialDelay());
+  EXPECT_EQ(base::Hours(5), config->NextCheckDelay());
+  EXPECT_EQ(base::Minutes(30), config->OnDemandDelay());
+  EXPECT_EQ(base::Minutes(15), config->UpdateDelay());
 
   // Test the fast-update timings.
   cmdline.AppendSwitchASCII("--component-updater", "fast-update");
   config = std::make_unique<ConfiguratorImpl>(
       ComponentUpdaterCommandLineConfigPolicy(&cmdline), false);
-  CHECK_EQ(base::Seconds(10), config->InitialDelay());
-  CHECK_EQ(base::Hours(5), config->NextCheckDelay());
-  CHECK_EQ(base::Seconds(2), config->OnDemandDelay());
-  CHECK_EQ(base::Seconds(10), config->UpdateDelay());
+  EXPECT_EQ(base::Seconds(10), config->InitialDelay());
+  EXPECT_EQ(base::Hours(5), config->NextCheckDelay());
+  EXPECT_EQ(base::Seconds(2), config->OnDemandDelay());
+  EXPECT_EQ(base::Seconds(10), config->UpdateDelay());
 }
 
 TEST_F(ComponentUpdaterConfiguratorImplTest, FastUpdateWithCustomPolicy) {
@@ -66,10 +72,10 @@
 
   std::unique_ptr<ConfiguratorImpl> config = std::make_unique<ConfiguratorImpl>(
       DefaultCommandLineConfigPolicy(), false);
-  CHECK_EQ(base::Minutes(1), config->InitialDelay());
-  CHECK_EQ(base::Hours(5), config->NextCheckDelay());
-  CHECK_EQ(base::Minutes(30), config->OnDemandDelay());
-  CHECK_EQ(base::Minutes(15), config->UpdateDelay());
+  EXPECT_EQ(base::Minutes(1), config->InitialDelay());
+  EXPECT_EQ(base::Hours(5), config->NextCheckDelay());
+  EXPECT_EQ(base::Minutes(30), config->OnDemandDelay());
+  EXPECT_EQ(base::Minutes(15), config->UpdateDelay());
 
   // Test the fast-update timings.
   class FastUpdateCommandLineConfigurator
@@ -81,16 +87,16 @@
   };
   config = std::make_unique<ConfiguratorImpl>(
       FastUpdateCommandLineConfigurator(), false);
-  CHECK_EQ(base::Seconds(10), config->InitialDelay());
-  CHECK_EQ(base::Hours(5), config->NextCheckDelay());
-  CHECK_EQ(base::Seconds(2), config->OnDemandDelay());
-  CHECK_EQ(base::Seconds(10), config->UpdateDelay());
+  EXPECT_EQ(base::Seconds(10), config->InitialDelay());
+  EXPECT_EQ(base::Hours(5), config->NextCheckDelay());
+  EXPECT_EQ(base::Seconds(2), config->OnDemandDelay());
+  EXPECT_EQ(base::Seconds(10), config->UpdateDelay());
 }
 
 TEST_F(ComponentUpdaterConfiguratorImplTest, InitialDelay) {
   std::unique_ptr<ConfiguratorImpl> config = std::make_unique<ConfiguratorImpl>(
       update_client::CommandLineConfigPolicy(), false);
-  CHECK_EQ(base::Minutes(1), config->InitialDelay());
+  EXPECT_EQ(base::Minutes(1), config->InitialDelay());
 
   class CommandLineConfigPolicy
       : public update_client::CommandLineConfigPolicy {
@@ -120,21 +126,21 @@
     CommandLineConfigPolicy clcp;
     clcp.set_fast_update(true);
     config = std::make_unique<ConfiguratorImpl>(clcp, false);
-    CHECK_EQ(base::Seconds(10), config->InitialDelay());
+    EXPECT_EQ(base::Seconds(10), config->InitialDelay());
   }
 
   {
     CommandLineConfigPolicy clcp;
     clcp.set_fast_update(false);
     config = std::make_unique<ConfiguratorImpl>(clcp, false);
-    CHECK_EQ(base::Minutes(1), config->InitialDelay());
+    EXPECT_EQ(base::Minutes(1), config->InitialDelay());
   }
 
   {
     CommandLineConfigPolicy clcp;
     clcp.set_initial_delay(base::Minutes(2));
     config = std::make_unique<ConfiguratorImpl>(clcp, false);
-    CHECK_EQ(base::Minutes(2), config->InitialDelay());
+    EXPECT_EQ(base::Minutes(2), config->InitialDelay());
   }
 
   {
@@ -145,7 +151,7 @@
                                     "initial-delay=3.14");
     config = std::make_unique<ConfiguratorImpl>(
         ComponentUpdaterCommandLineConfigPolicy(command_line), false);
-    CHECK_EQ(base::Seconds(3.14), config->InitialDelay());
+    EXPECT_EQ(base::Seconds(3.14), config->InitialDelay());
   }
 }
 
@@ -186,4 +192,24 @@
   EXPECT_EQ("dev", extra_request_params["testsource"]);
 }
 
+TEST_F(ComponentUpdaterConfiguratorImplTest, MeteredConnection) {
+  std::unique_ptr<net::test::MockNetworkChangeNotifier>
+      network_change_notifier = net::test::MockNetworkChangeNotifier::Create();
+  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
+  std::unique_ptr<ConfiguratorImpl> config = std::make_unique<ConfiguratorImpl>(
+      ComponentUpdaterCommandLineConfigPolicy(&cmdline), false);
+
+  network_change_notifier->SetConnectionCost(
+      net::NetworkChangeNotifier::CONNECTION_COST_UNKNOWN);
+  EXPECT_EQ(false, config->IsConnectionMetered());
+
+  network_change_notifier->SetConnectionCost(
+      net::NetworkChangeNotifier::CONNECTION_COST_METERED);
+  EXPECT_EQ(true, config->IsConnectionMetered());
+
+  network_change_notifier->SetConnectionCost(
+      net::NetworkChangeNotifier::CONNECTION_COST_UNMETERED);
+  EXPECT_EQ(false, config->IsConnectionMetered());
+}
+
 }  // namespace component_updater
diff --git a/components/optimization_guide/internal b/components/optimization_guide/internal
index f35015d..22d6e7c 160000
--- a/components/optimization_guide/internal
+++ b/components/optimization_guide/internal
@@ -1 +1 @@
-Subproject commit f35015d6f691b1aadc035cd1644aa3bc19a1a92a
+Subproject commit 22d6e7c48e3a433786898acedc2730a9b1c93e6c
diff --git a/components/safe_browsing/content/browser/safe_browsing_blocking_page.cc b/components/safe_browsing/content/browser/safe_browsing_blocking_page.cc
index d6bf88e2..faa64d5 100644
--- a/components/safe_browsing/content/browser/safe_browsing_blocking_page.cc
+++ b/components/safe_browsing/content/browser/safe_browsing_blocking_page.cc
@@ -131,11 +131,8 @@
 }
 
 void SafeBrowsingBlockingPage::OnInterstitialClosing() {
-  if (base::FeatureList::IsEnabled(safe_browsing::kAntiPhishingTelemetry) ||
-      base::FeatureList::IsEnabled(safe_browsing::kRedWarningSurvey)) {
-    interstitial_interactions_ =
-        sb_error_ui()->get_interstitial_interaction_data();
-  }
+  interstitial_interactions_ =
+      sb_error_ui()->get_interstitial_interaction_data();
 
   // If this is a phishing interstitial and the user did not make a decision
   // through the UI, record that interaction in UMA
@@ -143,11 +140,8 @@
     controller()->metrics_helper()->RecordUserInteraction(
         security_interstitials::MetricsHelper::CLOSE_INTERSTITIAL_WITHOUT_UI);
 
-    // If kAntiPhishingTelemetry is enabled, add
-    // CMD_CLOSE_INTERSTITIAL_WITHOUT_UI interaction to interactions.
-    if (interstitial_interactions_ &&
-        (base::FeatureList::IsEnabled(safe_browsing::kAntiPhishingTelemetry) ||
-         base::FeatureList::IsEnabled(safe_browsing::kRedWarningSurvey))) {
+    // Add CMD_CLOSE_INTERSTITIAL_WITHOUT_UI interaction to interactions.
+    if (interstitial_interactions_) {
       interstitial_interactions_->insert_or_assign(
           security_interstitials::SecurityInterstitialCommand::
               CMD_CLOSE_INTERSTITIAL_WITHOUT_UI,
@@ -192,11 +186,9 @@
   if (num_visits >= 0) {
     report->set_repeat_visit(num_visits > 0);
   }
-  if ((base::FeatureList::IsEnabled(safe_browsing::kAntiPhishingTelemetry) ||
-       base::FeatureList::IsEnabled(safe_browsing::kRedWarningSurvey)) &&
-      (report->type() == ClientSafeBrowsingReportRequest::URL_PHISHING ||
-       report->type() ==
-           ClientSafeBrowsingReportRequest::URL_CLIENT_SIDE_PHISHING)) {
+  if (report->type() == ClientSafeBrowsingReportRequest::URL_PHISHING ||
+      report->type() ==
+          ClientSafeBrowsingReportRequest::URL_CLIENT_SIDE_PHISHING) {
     client_report_utils::FillInterstitialInteractionsHelper(report.get(),
                                                             interactions);
   }
@@ -234,11 +226,8 @@
 
   // Finish computing threat details. TriggerManager will decide if its safe to
   // send the report.
-  if (base::FeatureList::IsEnabled(safe_browsing::kAntiPhishingTelemetry) ||
-      base::FeatureList::IsEnabled(safe_browsing::kRedWarningSurvey)) {
-    trigger_manager_->SetInterstitialInteractions(
-        std::move(interstitial_interactions_));
-  }
+  trigger_manager_->SetInterstitialInteractions(
+      std::move(interstitial_interactions_));
   bool is_hats_candidate = false;
   if (base::FeatureList::IsEnabled(kRedWarningSurvey)) {
     is_hats_candidate =
diff --git a/components/safe_browsing/content/browser/threat_details.cc b/components/safe_browsing/content/browser/threat_details.cc
index 40ae3ae..84841dc 100644
--- a/components/safe_browsing/content/browser/threat_details.cc
+++ b/components/safe_browsing/content/browser/threat_details.cc
@@ -831,9 +831,6 @@
 }
 
 bool ThreatDetails::ShouldFillInterstitialInteractions() {
-  if (!base::FeatureList::IsEnabled(safe_browsing::kAntiPhishingTelemetry)) {
-    return false;
-  }
   static constexpr auto valid_report_types =
       base::MakeFixedFlatSet<ClientSafeBrowsingReportRequest::ReportType>(
           {ClientSafeBrowsingReportRequest::URL_PHISHING,
diff --git a/components/safe_browsing/core/common/features.cc b/components/safe_browsing/core/common/features.cc
index f2b89cd1..781b584 100644
--- a/components/safe_browsing/core/common/features.cc
+++ b/components/safe_browsing/core/common/features.cc
@@ -29,10 +29,6 @@
              "AddWarningShownTSToClientSafeBrowsingReport",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
-BASE_FEATURE(kAntiPhishingTelemetry,
-             "AntiPhishingTelemetry",
-             base::FEATURE_ENABLED_BY_DEFAULT);
-
 BASE_FEATURE(kClientSideDetectionKillswitch,
              "ClientSideDetectionKillswitch",
              base::FEATURE_DISABLED_BY_DEFAULT);
@@ -303,7 +299,6 @@
 } kExperimentalFeatures[]{
     {&kAdSamplerTriggerFeature, false},
     {&kAddWarningShownTSToClientSafeBrowsingReport, false},
-    {&kAntiPhishingTelemetry, false},
     {&kClientSideDetectionKillswitch, true},
     {&kCreateWarningShownClientSafeBrowsingReports, false},
     {&kDelayedWarnings, true},
diff --git a/components/safe_browsing/core/common/features.h b/components/safe_browsing/core/common/features.h
index 6fdb311..c0d57e0 100644
--- a/components/safe_browsing/core/common/features.h
+++ b/components/safe_browsing/core/common/features.h
@@ -21,9 +21,6 @@
 // Enables adding warning shown timestamp to client safe browsing report.
 BASE_DECLARE_FEATURE(kAddWarningShownTSToClientSafeBrowsingReport);
 
-// Enables logging new phishing prevention data.
-BASE_DECLARE_FEATURE(kAntiPhishingTelemetry);
-
 // Killswitch for client side phishing detection. Since client side models are
 // run on a large fraction of navigations, crashes due to the model are very
 // impactful, even if only a small fraction of users have a bad version of the
diff --git a/components/security_interstitials/core/safe_browsing_loud_error_ui.cc b/components/security_interstitials/core/safe_browsing_loud_error_ui.cc
index 7811c8a1..b1892cce 100644
--- a/components/security_interstitials/core/safe_browsing_loud_error_ui.cc
+++ b/components/security_interstitials/core/safe_browsing_loud_error_ui.cc
@@ -139,10 +139,7 @@
 
 void SafeBrowsingLoudErrorUI::HandleCommand(
     SecurityInterstitialCommand command) {
-  if (base::FeatureList::IsEnabled(safe_browsing::kAntiPhishingTelemetry) ||
-      base::FeatureList::IsEnabled(safe_browsing::kRedWarningSurvey)) {
-    UpdateInterstitialInteractionData(command);
-  }
+  UpdateInterstitialInteractionData(command);
 
   switch (command) {
     case CMD_PROCEED: {
diff --git a/components/update_client/component.cc b/components/update_client/component.cc
index d3d2652..85c94b7 100644
--- a/components/update_client/component.cc
+++ b/components/update_client/component.cc
@@ -921,7 +921,9 @@
   component.is_update_available_ = true;
   component.NotifyObservers(Events::COMPONENT_UPDATE_FOUND);
 
-  if (!component.crx_component()->updates_enabled) {
+  if (!component.crx_component()->updates_enabled ||
+      (!component.crx_component()->allow_updates_on_metered_connection &&
+       component.config()->IsConnectionMetered())) {
     component.error_category_ = ErrorCategory::kService;
     component.error_code_ = static_cast<int>(ServiceError::UPDATE_DISABLED);
     component.extra_code1_ = 0;
diff --git a/components/update_client/configurator.h b/components/update_client/configurator.h
index ddba835..0382f5022 100644
--- a/components/update_client/configurator.h
+++ b/components/update_client/configurator.h
@@ -149,6 +149,8 @@
   // puffin patches.
   virtual absl::optional<base::FilePath> GetCrxCachePath() const = 0;
 
+  virtual bool IsConnectionMetered() const = 0;
+
  protected:
   friend class base::RefCountedThreadSafe<Configurator>;
 
diff --git a/components/update_client/test_configurator.cc b/components/update_client/test_configurator.cc
index ea72b53..c489e25 100644
--- a/components/update_client/test_configurator.cc
+++ b/components/update_client/test_configurator.cc
@@ -12,6 +12,7 @@
 #include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
 #include "base/functional/bind.h"
+#include "base/memory/scoped_refptr.h"
 #include "base/path_service.h"
 #include "base/time/time.h"
 #include "base/version.h"
@@ -63,7 +64,8 @@
               test_shared_loader_factory_,
               base::BindRepeating([](const GURL& url) { return false; }))),
       updater_state_provider_(base::BindRepeating(
-          [](bool /*is_machine*/) { return UpdaterStateAttributes(); })) {
+          [](bool /*is_machine*/) { return UpdaterStateAttributes(); })),
+      is_network_connection_metered_(false) {
   std::ignore = crx_cache_root_temp_dir_.CreateUniqueTempDir();
 }
 
@@ -86,15 +88,17 @@
 }
 
 std::vector<GURL> TestConfigurator::UpdateUrl() const {
-  if (!update_check_urls_.empty())
+  if (!update_check_urls_.empty()) {
     return update_check_urls_;
+  }
 
   return MakeDefaultUrls();
 }
 
 std::vector<GURL> TestConfigurator::PingUrl() const {
-  if (!ping_url_.is_empty())
+  if (!ping_url_.is_empty()) {
     return std::vector<GURL>(1, ping_url_);
+  }
 
   return UpdateUrl();
 }
@@ -192,6 +196,10 @@
       crx_cache_root_temp_dir_.GetPath().AppendASCII("crx_cache"));
 }
 
+bool TestConfigurator::IsConnectionMetered() const {
+  return is_network_connection_metered_;
+}
+
 void TestConfigurator::SetOnDemandTime(base::TimeDelta time) {
   ondemand_time_ = time;
 }
@@ -231,6 +239,11 @@
   is_machine_externally_managed_ = is_machine_externally_managed;
 }
 
+void TestConfigurator::SetIsNetworkConnectionMetered(
+    bool is_network_connection_metered) {
+  is_network_connection_metered_ = is_network_connection_metered;
+}
+
 void TestConfigurator::SetUpdaterStateProvider(
     UpdaterStateProvider update_state_provider) {
   updater_state_provider_ = update_state_provider;
diff --git a/components/update_client/test_configurator.h b/components/update_client/test_configurator.h
index 63418d2..f6d7db5 100644
--- a/components/update_client/test_configurator.h
+++ b/components/update_client/test_configurator.h
@@ -105,6 +105,7 @@
   absl::optional<bool> IsMachineExternallyManaged() const override;
   UpdaterStateProvider GetUpdaterStateProvider() const override;
   absl::optional<base::FilePath> GetCrxCachePath() const override;
+  bool IsConnectionMetered() const override;
 
   void SetOnDemandTime(base::TimeDelta seconds);
   void SetInitialDelay(base::TimeDelta seconds);
@@ -117,6 +118,7 @@
       scoped_refptr<CrxDownloaderFactory> crx_downloader_factory);
   void SetIsMachineExternallyManaged(
       absl::optional<bool> is_machine_externally_managed);
+  void SetIsNetworkConnectionMetered(bool is_network_connection_metered);
   void SetUpdaterStateProvider(UpdaterStateProvider update_state_provider);
   network::TestURLLoaderFactory* test_url_loader_factory() {
     return &test_url_loader_factory_;
@@ -146,6 +148,7 @@
   scoped_refptr<CrxDownloaderFactory> crx_downloader_factory_;
   UpdaterStateProvider updater_state_provider_;
   absl::optional<bool> is_machine_externally_managed_;
+  bool is_network_connection_metered_;
   base::ScopedTempDir crx_cache_root_temp_dir_;
 };
 
diff --git a/components/update_client/update_checker.cc b/components/update_client/update_checker.cc
index 79623ad2..a2111ce4 100644
--- a/components/update_client/update_checker.cc
+++ b/components/update_client/update_checker.cc
@@ -183,10 +183,11 @@
     sent_ids.push_back(app_id);
 
     std::string install_source;
-    if (!crx_component->install_source.empty())
+    if (!crx_component->install_source.empty()) {
       install_source = crx_component->install_source;
-    else if (component->is_foreground())
+    } else if (component->is_foreground()) {
       install_source = "ondemand";
+    }
 
     apps.push_back(MakeProtocolApp(
         app_id, crx_component->version, crx_component->ap, crx_component->brand,
@@ -195,16 +196,20 @@
         crx_component->installer_attributes, metadata_->GetCohort(app_id),
         metadata_->GetCohortHint(app_id), metadata_->GetCohortName(app_id),
         crx_component->channel, crx_component->disabled_reasons,
-        MakeProtocolUpdateCheck(!crx_component->updates_enabled,
-                                crx_component->target_version_prefix,
-                                crx_component->rollback_allowed,
-                                crx_component->same_version_update_allowed),
+        MakeProtocolUpdateCheck(
+            !crx_component->updates_enabled ||
+                (!crx_component->allow_updates_on_metered_connection &&
+                 config_->IsConnectionMetered()),
+            crx_component->target_version_prefix,
+            crx_component->rollback_allowed,
+            crx_component->same_version_update_allowed),
         [](const std::string& install_data_index)
             -> std::vector<protocol_request::Data> {
-          if (install_data_index.empty())
+          if (install_data_index.empty()) {
             return {};
-          else
+          } else {
             return {{"install", install_data_index, ""}};
+          }
         }(crx_component->install_data_index),
         MakeProtocolPing(app_id, metadata_,
                          active_ids.find(app_id) != active_ids.end()),
@@ -286,14 +291,17 @@
   const int daynum = results.daystart_elapsed_days;
   for (const auto& result : results.list) {
     auto entry = result.cohort_attrs.find(ProtocolParser::Result::kCohort);
-    if (entry != result.cohort_attrs.end())
+    if (entry != result.cohort_attrs.end()) {
       metadata_->SetCohort(result.extension_id, entry->second);
+    }
     entry = result.cohort_attrs.find(ProtocolParser::Result::kCohortName);
-    if (entry != result.cohort_attrs.end())
+    if (entry != result.cohort_attrs.end()) {
       metadata_->SetCohortName(result.extension_id, entry->second);
+    }
     entry = result.cohort_attrs.find(ProtocolParser::Result::kCohortHint);
-    if (entry != result.cohort_attrs.end())
+    if (entry != result.cohort_attrs.end()) {
       metadata_->SetCohortHint(result.extension_id, entry->second);
+    }
   }
 
   base::OnceClosure reply =
diff --git a/components/update_client/update_checker_unittest.cc b/components/update_client/update_checker_unittest.cc
index 5b5f2d7..36726b0a 100644
--- a/components/update_client/update_checker_unittest.cc
+++ b/components/update_client/update_checker_unittest.cc
@@ -74,7 +74,8 @@
   std::unique_ptr<Component> MakeComponent(const std::string& brand) const;
   std::unique_ptr<Component> MakeComponent(
       const std::string& brand,
-      const std::string& install_data_index) const;
+      const std::string& install_data_index,
+      bool allow_updates_on_metered_connection) const;
   absl::optional<base::Value::Dict> ParseRequest(int request_number);
   base::Value GetFirstAppAsValue(const base::Value::Dict& request);
   base::Value::Dict GetFirstAppAsDict(const base::Value::Dict& request);
@@ -157,8 +158,9 @@
 }
 
 void UpdateCheckerTest::Quit() {
-  if (!quit_closure_.is_null())
+  if (!quit_closure_.is_null()) {
     std::move(quit_closure_).Run();
+  }
 }
 
 void UpdateCheckerTest::UpdateCheckComplete(
@@ -189,12 +191,13 @@
 
 std::unique_ptr<Component> UpdateCheckerTest::MakeComponent(
     const std::string& brand) const {
-  return MakeComponent(brand, {});
+  return MakeComponent(brand, {}, true);
 }
 
 std::unique_ptr<Component> UpdateCheckerTest::MakeComponent(
     const std::string& brand,
-    const std::string& install_data_index) const {
+    const std::string& install_data_index,
+    bool allow_updates_on_metered_connection) const {
   CrxComponent crx_component;
   crx_component.app_id = "jebgalgnebhfojomionfpkfelancnnkf";
   crx_component.brand = brand;
@@ -204,6 +207,8 @@
   crx_component.installer = nullptr;
   crx_component.version = base::Version("0.9");
   crx_component.fingerprint = "fp1";
+  crx_component.allow_updates_on_metered_connection =
+      allow_updates_on_metered_connection;
 
   auto component = std::make_unique<Component>(*update_context_, kUpdateItemId);
   component->state_ = std::make_unique<Component::StateNew>(component.get());
@@ -258,7 +263,7 @@
   update_checker_ = UpdateChecker::Create(config_, metadata_.get());
 
   update_context_->components[kUpdateItemId] =
-      MakeComponent("TEST", "foobar_install_data_index");
+      MakeComponent("TEST", "foobar_install_data_index", true);
 
   auto& component = update_context_->components[kUpdateItemId];
   component->crx_component_->installer_attributes["ap"] = "some_ap";
@@ -747,14 +752,14 @@
   ASSERT_EQ(3, post_interceptor_->GetCount())
       << post_interceptor_->GetRequestsAsString();
 
-    {
+  {
     absl::optional<base::Value::Dict> root = ParseRequest(0);
     ASSERT_TRUE(root);
     const base::Value app = GetFirstAppAsValue(root.value());
     EXPECT_EQ(10, app.GetDict().FindIntByDottedPath("ping.a").value());
     EXPECT_EQ(-2, app.GetDict().FindIntByDottedPath("ping.r").value());
-    }
-    {
+  }
+  {
     absl::optional<base::Value::Dict> root = ParseRequest(1);
     ASSERT_TRUE(root);
     const base::Value app = GetFirstAppAsValue(root.value());
@@ -762,15 +767,15 @@
     EXPECT_EQ(3383, app.GetDict().FindByDottedPath("ping.rd")->GetInt());
     EXPECT_TRUE(
         app.GetDict().FindByDottedPath("ping.ping_freshness")->is_string());
-    }
-    {
+  }
+  {
     absl::optional<base::Value::Dict> root = ParseRequest(2);
     ASSERT_TRUE(root);
     const base::Value app = GetFirstAppAsValue(root.value());
     EXPECT_EQ(3383, app.GetDict().FindByDottedPath("ping.rd")->GetInt());
     EXPECT_TRUE(
         app.GetDict().FindByDottedPath("ping.ping_freshness")->is_string());
-    }
+  }
 }
 
 TEST_P(UpdateCheckerTest, UpdateCheckInstallSource) {
@@ -1074,6 +1079,72 @@
   }
 }
 
+TEST_P(UpdateCheckerTest, UpdateDisabledByMeteredConnection) {
+  update_checker_ = UpdateChecker::Create(config_, metadata_.get());
+
+  update_context_->components[kUpdateItemId] =
+      MakeComponent("TEST", "foobar_install_data_index", false);
+
+  auto& component = update_context_->components[kUpdateItemId];
+  auto crx_component = component->crx_component();
+
+  // Ignore this test parameter to keep the test simple.
+  update_context_->is_foreground = false;
+  {
+    // Tests the scenario where:
+    //  * the component updates are enabled on a non-metered connection.
+    // Expects the the update check to not include the "updatedisabled"
+    // attribute.
+    config_->SetIsNetworkConnectionMetered(false);
+    auto post_interceptor = std::make_unique<URLLoaderPostInterceptor>(
+        config_->test_url_loader_factory());
+    EXPECT_TRUE(post_interceptor->ExpectRequest(
+        std::make_unique<PartialMatch>("updatecheck"),
+        GetTestFilePath("updatecheck_reply_1.json")));
+    update_checker_->CheckForUpdates(
+        update_context_, {},
+        base::BindOnce(&UpdateCheckerTest::UpdateCheckComplete,
+                       base::Unretained(this)));
+    RunThreads();
+    const auto& request = post_interceptor->GetRequestBody(0);
+    const auto root = base::JSONReader::Read(request);
+    ASSERT_TRUE(root);
+    const base::Value::Dict app = GetFirstAppAsDict(root->GetDict());
+    EXPECT_EQ(kUpdateItemId, CHECK_DEREF(app.FindString("appid")));
+    EXPECT_EQ("0.9", CHECK_DEREF(app.FindString("version")));
+    EXPECT_EQ(true, app.FindBool("enabled"));
+    EXPECT_TRUE(app.FindDict("updatecheck")->empty());
+  }
+  {
+    // Tests the scenario where:
+    //  * updates are disabled due to a metered network connection.
+    // Expects the update check to include the "updatedisabled" attribute.
+    config_->SetIsNetworkConnectionMetered(true);
+    component->set_crx_component(*crx_component);
+    auto post_interceptor = std::make_unique<URLLoaderPostInterceptor>(
+        config_->test_url_loader_factory());
+    EXPECT_TRUE(post_interceptor->ExpectRequest(
+        std::make_unique<PartialMatch>("updatecheck"),
+        GetTestFilePath("updatecheck_reply_1.json")));
+    update_checker_->CheckForUpdates(
+        update_context_, {},
+        base::BindOnce(&UpdateCheckerTest::UpdateCheckComplete,
+                       base::Unretained(this)));
+    RunThreads();
+    const auto& request = post_interceptor->GetRequestBody(0);
+    const auto root = base::JSONReader::Read(request);
+    ASSERT_TRUE(root);
+    const base::Value app_as_val = GetFirstAppAsValue(root->GetDict());
+    const base::Value::Dict app = GetFirstAppAsDict(root->GetDict());
+    EXPECT_EQ(kUpdateItemId, CHECK_DEREF(app.FindString("appid")));
+    EXPECT_EQ("0.9", CHECK_DEREF(app.FindString("version")));
+    EXPECT_EQ(true, app.FindBool("enabled"));
+    EXPECT_TRUE(app_as_val.GetDict()
+                    .FindBoolByDottedPath("updatecheck.updatedisabled")
+                    .value());
+  }
+}
+
 TEST_P(UpdateCheckerTest, SameVersionUpdateAllowed) {
   update_checker_ = UpdateChecker::Create(config_, metadata_.get());
 
diff --git a/components/update_client/update_client.h b/components/update_client/update_client.h
index 3d34920..a6ea29b 100644
--- a/components/update_client/update_client.h
+++ b/components/update_client/update_client.h
@@ -378,6 +378,9 @@
   // Specifies that this CRX can be cached for differential updates.
   // The default for this value is |true|.
   bool allow_cached_copies = true;
+
+  // Specifies whether updates can be initiated on metered network connections.
+  bool allow_updates_on_metered_connection = true;
 };
 
 // Called when a non-blocking call of UpdateClient completes.
diff --git a/docs/updater/functional_spec.md b/docs/updater/functional_spec.md
index 1f440bb5..3d790df 100644
--- a/docs/updater/functional_spec.md
+++ b/docs/updater/functional_spec.md
@@ -1448,8 +1448,9 @@
 24 times but never had a product (besides itself) registered for updates.
 
 The updater uninstaller removes all updater files, registry keys, RPC hooks,
-scheduled tasks, and so forth from the file system, except that it leaves a
-small log file in its data directory.
+scheduled tasks, and so forth from the system, except that:
+*   it leaves a small log file in its data directory.
+*   it leaves the Clients registry key in Windows registry.
 
 ## Associated Tools
 
diff --git a/ios/chrome/browser/component_updater/model/ios_component_updater_configurator.mm b/ios/chrome/browser/component_updater/model/ios_component_updater_configurator.mm
index 1294a49..1608d7d4 100644
--- a/ios/chrome/browser/component_updater/model/ios_component_updater_configurator.mm
+++ b/ios/chrome/browser/component_updater/model/ios_component_updater_configurator.mm
@@ -13,6 +13,7 @@
 
 #import "base/containers/flat_map.h"
 #import "base/files/file_path.h"
+#import "base/memory/scoped_refptr.h"
 #import "base/path_service.h"
 #import "base/version.h"
 #import "components/component_updater/component_updater_command_line_config_policy.h"
@@ -72,6 +73,7 @@
   std::optional<bool> IsMachineExternallyManaged() const override;
   update_client::UpdaterStateProvider GetUpdaterStateProvider() const override;
   std::optional<base::FilePath> GetCrxCachePath() const override;
+  bool IsConnectionMetered() const override;
 
  private:
   friend class base::RefCountedThreadSafe<IOSConfigurator>;
@@ -235,6 +237,10 @@
   return path.Append(FILE_PATH_LITERAL("ios_crx_cache"));
 }
 
+bool IOSConfigurator::IsConnectionMetered() const {
+  return configurator_impl_.IsConnectionMetered();
+}
+
 }  // namespace
 
 scoped_refptr<update_client::Configurator> MakeIOSComponentUpdaterConfigurator(
diff --git a/ios/chrome/browser/ui/ntp/feed_top_section/feed_top_section_mediator.mm b/ios/chrome/browser/ui/ntp/feed_top_section/feed_top_section_mediator.mm
index 539e68c..a24399b 100644
--- a/ios/chrome/browser/ui/ntp/feed_top_section/feed_top_section_mediator.mm
+++ b/ios/chrome/browser/ui/ntp/feed_top_section/feed_top_section_mediator.mm
@@ -145,16 +145,12 @@
 
 - (void)notificationsPromoViewMainButtonWasTapped {
   // Show the Notifications promo alert.
-  PushNotificationService* service =
-      GetApplicationContext()->GetPushNotificationService();
-  id<SystemIdentity> identity = self.authenticationService->GetPrimaryIdentity(
-      signin::ConsentLevel::kSignin);
   __weak FeedTopSectionMediator* weakSelf = self;
   // Request displaying the OS notifications permission prompt.
   [PushNotificationUtil requestPushNotificationPermission:^(
                             BOOL granted, BOOL promptShown, NSError* error) {
     if (error) {
-      [self updateFeedTopSectionWhenClosed];
+      [self closeNotificationPromoAndEnablePref:NO];
       return;
     }
     if (!promptShown && !granted) {
@@ -165,42 +161,25 @@
       dispatch_async(dispatch_get_main_queue(), ^{
         [weakSelf
                 .notificationsPresenter presentPushNotificationPermissionAlert];
-        [self updateFeedTopSectionWhenClosed];
       });
       return;
     }
     if (promptShown && granted) {
       // If the OS prompt is shown and the user granted notifications access,
       // save the preference and close the promo.
-      // This callback can be executed on a background thread, make sure the UI
-      // is updated on the main thread.
-      dispatch_async(dispatch_get_main_queue(), ^{
-        service->SetPreference(identity.gaiaID,
-                               PushNotificationClientId::kContent, true);
-        [self updateFeedTopSectionWhenClosed];
-      });
-      return;
-    }
-    if (promptShown && !granted) {
-      // If the OS prompt is shown and the user denied notifications access,
-      // close the promo.
-      // This callback can be executed on a background thread, make sure the UI
-      // is updated on the main thread.
-      dispatch_async(dispatch_get_main_queue(), ^{
-        [self updateFeedTopSectionWhenClosed];
-      });
+      [self closeNotificationPromoAndEnablePref:YES];
       return;
     }
     if (!promptShown && granted) {
       // If the OS prompt has been previously shown but notifications are not
       // active on Chrome activate the notifications. This is an edge case.
-      // This callback can be executed on a background thread, make sure the UI
-      // is updated on the main thread.
-      dispatch_async(dispatch_get_main_queue(), ^{
-        service->SetPreference(identity.gaiaID,
-                               PushNotificationClientId::kContent, true);
-        [self updateFeedTopSectionWhenClosed];
-      });
+      [self closeNotificationPromoAndEnablePref:YES];
+      return;
+    }
+    if (promptShown && !granted) {
+      // If the OS prompt is shown and the user denied notifications access,
+      // close the promo.
+      [self closeNotificationPromoAndEnablePref:NO];
       return;
     }
   }];
@@ -208,6 +187,25 @@
 
 #pragma mark - Private
 
+// Helper method to close the promo on the main thread. Takes `enablePref` as a
+// parameter which toggles the pref ON only.
+- (void)closeNotificationPromoAndEnablePref:(BOOL)enablePref {
+  // This callback can be executed on a background thread, make sure the UI
+  // is updated on the main thread.
+  dispatch_async(dispatch_get_main_queue(), ^{
+    if (enablePref) {
+      PushNotificationService* service =
+          GetApplicationContext()->GetPushNotificationService();
+      id<SystemIdentity> identity =
+          self.authenticationService->GetPrimaryIdentity(
+              signin::ConsentLevel::kSignin);
+      service->SetPreference(identity.gaiaID,
+                             PushNotificationClientId::kContent, true);
+    }
+    [self updateFeedTopSectionWhenClosed];
+  });
+}
+
 // Handles closing the promo, and the NTP and Feed Top Section layout when the
 // promo is closed.
 - (void)updateFeedTopSectionWhenClosed {
diff --git a/ios/web_view/internal/component_updater/web_view_component_updater_configurator.mm b/ios/web_view/internal/component_updater/web_view_component_updater_configurator.mm
index 8aee2276..eface87 100644
--- a/ios/web_view/internal/component_updater/web_view_component_updater_configurator.mm
+++ b/ios/web_view/internal/component_updater/web_view_component_updater_configurator.mm
@@ -3,7 +3,6 @@
 // found in the LICENSE file.
 
 #import "ios/web_view/internal/component_updater/web_view_component_updater_configurator.h"
-#import "base/time/time.h"
 
 #import <stdint.h>
 
@@ -15,6 +14,7 @@
 #import "base/containers/flat_map.h"
 #import "base/files/file_path.h"
 #import "base/path_service.h"
+#import "base/time/time.h"
 #import "base/version.h"
 #import "components/component_updater/component_updater_command_line_config_policy.h"
 #import "components/component_updater/configurator_impl.h"
@@ -75,6 +75,7 @@
   std::optional<bool> IsMachineExternallyManaged() const override;
   update_client::UpdaterStateProvider GetUpdaterStateProvider() const override;
   std::optional<base::FilePath> GetCrxCachePath() const override;
+  bool IsConnectionMetered() const override;
 
  private:
   friend class base::RefCountedThreadSafe<WebViewConfigurator>;
@@ -240,6 +241,10 @@
   return path.Append(FILE_PATH_LITERAL("ios_webview_crx_cache"));
 }
 
+bool WebViewConfigurator::IsConnectionMetered() const {
+  return configurator_impl_.IsConnectionMetered();
+}
+
 }  // namespace
 
 scoped_refptr<update_client::Configurator> MakeComponentUpdaterConfigurator(
diff --git a/ios_internal b/ios_internal
index d8d82ca..27d0f4f 160000
--- a/ios_internal
+++ b/ios_internal
@@ -1 +1 @@
-Subproject commit d8d82ca955498f2a3c907380b829aa6270a64b82
+Subproject commit 27d0f4f79518dfe9f8e15cf52f1130ba3a6a5958
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index 90bbf84..9454cf9 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -1437,6 +1437,21 @@
             ]
         }
     ],
+    "AutofillEnablePaymentsMandatoryReauth": [
+        {
+            "platforms": [
+                "ios"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "AutofillEnablePaymentsMandatoryReauth"
+                    ]
+                }
+            ]
+        }
+    ],
     "AutofillEnableSupportForParsingWithSharedLabels": [
         {
             "platforms": [
diff --git a/third_party/angle b/third_party/angle
index 82c0ba9..295eece6 160000
--- a/third_party/angle
+++ b/third_party/angle
@@ -1 +1 @@
-Subproject commit 82c0ba9350b582eeff34c8c17886ac6b638f281b
+Subproject commit 295eece61cce0f7721427d4a42e0fe1821c6d39f
diff --git a/third_party/blink/renderer/core/display_lock/display_lock_context.cc b/third_party/blink/renderer/core/display_lock/display_lock_context.cc
index b33a596..17f09746 100644
--- a/third_party/blink/renderer/core/display_lock/display_lock_context.cc
+++ b/third_party/blink/renderer/core/display_lock/display_lock_context.cc
@@ -581,7 +581,7 @@
   // We also need to notify the AX cache (if it exists) to update the childrens
   // of |element_| in the AX cache.
   if (auto* ax_cache = element_->GetDocument().ExistingAXObjectCache()) {
-    ax_cache->SubtreeIsAttached(element_);
+    ax_cache->RemoveSubtreeWhenSafe(element_);
   }
 
   // Schedule ContentVisibilityAutoStateChange event if needed.
diff --git a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_colorspace_bgra8unorm.https.html b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_colorspace_bgra8unorm.https.html
index c29e904..9976ea02 100644
--- a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_colorspace_bgra8unorm.https.html
+++ b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_colorspace_bgra8unorm.https.html
@@ -15,6 +15,7 @@
   <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
   <meta name="assert" content="WebGPU bgra8norm canvas with colorSpace set should be rendered correctly" />
   <link rel="match" href="./ref/canvas_colorspace-ref.html" />
+  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999">
   <script type="module">
     import { runColorSpaceTest } from './canvas_colorspace.html.js';
     runColorSpaceTest('bgra8unorm');
diff --git a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_colorspace_rgba16float.https.html b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_colorspace_rgba16float.https.html
index 2b868d6..c898d90 100644
--- a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_colorspace_rgba16float.https.html
+++ b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_colorspace_rgba16float.https.html
@@ -15,7 +15,7 @@
   <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
   <meta name="assert" content="WebGPU rgba16float canvas with colorSpace set should be rendered correctly" />
   <link rel="match" href="./ref/canvas_colorspace-ref.html" />
-  <meta name=fuzzy content="maxDifference=1;totalPixels=8192">
+  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999">
   <script type="module">
     import { runColorSpaceTest } from './canvas_colorspace.html.js';
     runColorSpaceTest('rgba16float');
diff --git a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_colorspace_rgba8unorm.https.html b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_colorspace_rgba8unorm.https.html
index 33fb6dc..eafd264e 100644
--- a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_colorspace_rgba8unorm.https.html
+++ b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_colorspace_rgba8unorm.https.html
@@ -15,6 +15,7 @@
   <link rel="help" href="https://gpuweb.github.io/gpuweb/" />
   <meta name="assert" content="WebGPU rgba8unorm canvas with colorSpace set should be rendered correctly" />
   <link rel="match" href="./ref/canvas_colorspace-ref.html" />
+  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999">
   <script type="module">
     import { runColorSpaceTest } from './canvas_colorspace.html.js';
     runColorSpaceTest('rgba8unorm');
diff --git a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_copy.https.html b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_copy.https.html
index 9633eb8..972ee26d 100644
--- a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_copy.https.html
+++ b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_copy.https.html
@@ -9,7 +9,7 @@
     content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
   />
   <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" />
-  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400">
+  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999">
   <style>
     body { background-color: #F0E68C; }
     #c-canvas { background-color: #8CF0E6; }
diff --git a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_draw.https.html b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_draw.https.html
index 8800d93c2..cde343ed 100644
--- a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_draw.https.html
+++ b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_bgra8unorm_premultiplied_draw.https.html
@@ -9,7 +9,7 @@
     content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
   />
   <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" />
-  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400">
+  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999">
   <style>
     body { background-color: #F0E68C; }
     #c-canvas { background-color: #8CF0E6; }
diff --git a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_copy.https.html b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_copy.https.html
index 77597b54..c3241d9a 100644
--- a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_copy.https.html
+++ b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_copy.https.html
@@ -9,7 +9,7 @@
     content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
   />
   <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" />
-  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400">
+  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999">
   <style>
     body { background-color: #F0E68C; }
     #c-canvas { background-color: #8CF0E6; }
diff --git a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_draw.https.html b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_draw.https.html
index 9b2baf5..80cca9b 100644
--- a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_draw.https.html
+++ b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba16float_premultiplied_draw.https.html
@@ -9,7 +9,7 @@
     content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
   />
   <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" />
-  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400">
+  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999">
   <style>
     body { background-color: #F0E68C; }
     #c-canvas { background-color: #8CF0E6; }
diff --git a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_copy.https.html b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_copy.https.html
index b5ca6a88..510ba1da 100644
--- a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_copy.https.html
+++ b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_copy.https.html
@@ -9,7 +9,7 @@
     content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
   />
   <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" />
-  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400">
+  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999">
   <style>
     body { background-color: #F0E68C; }
     #c-canvas { background-color: #8CF0E6; }
diff --git a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_draw.https.html b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_draw.https.html
index cacc4879..20655f6 100644
--- a/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_draw.https.html
+++ b/third_party/blink/web_tests/wpt_internal/webgpu/web_platform/reftests/canvas_composite_alpha_rgba8unorm_premultiplied_draw.https.html
@@ -9,7 +9,7 @@
     content="WebGPU canvas should have correct orientation, components, scaling, filtering, color space"
   />
   <link rel="match" href="./ref/canvas_composite_alpha_premultiplied-ref.html" />
-  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-400">
+  <meta name=fuzzy content="maxDifference=0-2;totalPixels=0-9999999">
   <style>
     body { background-color: #F0E68C; }
     #c-canvas { background-color: #8CF0E6; }
diff --git a/third_party/depot_tools b/third_party/depot_tools
index 86752e9..259976c 160000
--- a/third_party/depot_tools
+++ b/third_party/depot_tools
@@ -1 +1 @@
-Subproject commit 86752e9a55281200715749d75a88cf57bf2e7b01
+Subproject commit 259976c7483c321c122169d193e150310f4ea609
diff --git a/third_party/skia b/third_party/skia
index 063e339..dd40779 160000
--- a/third_party/skia
+++ b/third_party/skia
@@ -1 +1 @@
-Subproject commit 063e339bedfcfb9d7c921cd92d9a902ef008ce76
+Subproject commit dd4077962cd5456478501f733af12b5dfb17b3ea
diff --git a/third_party/webgpu-cts/src b/third_party/webgpu-cts/src
index f20c5f7b..ae15a59 160000
--- a/third_party/webgpu-cts/src
+++ b/third_party/webgpu-cts/src
@@ -1 +1 @@
-Subproject commit f20c5f7b8f53904edaa98651d764e1b8305d7c14
+Subproject commit ae15a59832989c22982acaeaccdf5d379afced62
diff --git a/third_party/webgpu-cts/ts_sources.txt b/third_party/webgpu-cts/ts_sources.txt
index 866365b..2fa63cf 100644
--- a/third_party/webgpu-cts/ts_sources.txt
+++ b/third_party/webgpu-cts/ts_sources.txt
@@ -146,6 +146,7 @@
 src/unittests/texture_ok.spec.ts
 src/webgpu/examples.spec.ts
 src/webgpu/listing.ts
+src/webgpu/multisample_info.ts
 src/webgpu/api/operation/labels.spec.ts
 src/webgpu/api/operation/onSubmittedWorkDone.spec.ts
 src/webgpu/api/operation/reflection.spec.ts
@@ -348,8 +349,9 @@
 src/webgpu/compat/api/validation/render_pipeline/vertex_state.spec.ts
 src/webgpu/compat/api/validation/texture/createTexture.spec.ts
 src/webgpu/compat/api/validation/texture/cubeArray.spec.ts
-src/webgpu/idl/exposed.html.ts
 src/webgpu/idl/idl_test.ts
+src/webgpu/idl/constructable.spec.ts
+src/webgpu/idl/exposed.html.ts
 src/webgpu/idl/constants/flags.spec.ts
 src/webgpu/shader/types.ts
 src/webgpu/shader/values.ts
@@ -647,6 +649,7 @@
 src/webgpu/shader/execution/memory_model/coherence.spec.ts
 src/webgpu/shader/execution/memory_model/weak.spec.ts
 src/webgpu/shader/execution/shader_io/compute_builtins.spec.ts
+src/webgpu/shader/execution/shader_io/fragment_builtins.spec.ts
 src/webgpu/shader/execution/shader_io/shared_structs.spec.ts
 src/webgpu/shader/execution/statement/increment_decrement.spec.ts
 src/webgpu/shader/validation/shader_validation_test.ts
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 2b9bf5b..f632df73 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -26914,6 +26914,8 @@
   <int value="-1520952503" label="SearchReadyOmnibox:enabled"/>
   <int value="-1520901408" label="FuseBoxDebug:disabled"/>
   <int value="-1520855274" label="PWAFullCodeCache:disabled"/>
+  <int value="-1520821533"
+      label="HoldingSpaceWallpaperNudgeForceEligibility:disabled"/>
   <int value="-1520645293" label="InterestFeedNoticeCardAutoDismiss:disabled"/>
   <int value="-1520630395" label="DefaultPassthroughCommandDecoder:disabled"/>
   <int value="-1517929127" label="DelegatedCompositing:enabled"/>
@@ -28035,6 +28037,8 @@
       label="ExperimentalAccessibilityColorEnhancementSettings:disabled"/>
   <int value="-1002537430" label="HudDisplayForPerformanceMetrics:disabled"/>
   <int value="-1002494650" label="WebviewTagMPArchBehavior:disabled"/>
+  <int value="-1002421482"
+      label="HoldingSpaceWallpaperNudgeForceEligibility:enabled"/>
   <int value="-1001837588" label="EnableAppReinstallZeroState:enabled"/>
   <int value="-1000695123"
       label="AccessibilitySelectToSpeakPageMigration:disabled"/>
@@ -30953,6 +30957,7 @@
   <int value="370486304" label="enable-origin-chip-on-srp"/>
   <int value="370926002" label="FirmwareUpdateUIV2:enabled"/>
   <int value="371268743" label="OmniboxUIExperimentVerticalMargin:disabled"/>
+  <int value="371496127" label="HoldingSpaceWallpaperNudge:enabled"/>
   <int value="372460068" label="QuickUnlockFingerprint:disabled"/>
   <int value="373177941" label="DockedMagnifier:enabled"/>
   <int value="373193557" label="LongPressBackNewDesign:enabled"/>
@@ -31948,6 +31953,7 @@
   <int value="845659238" label="DevicePosture:enabled"/>
   <int value="846887449" label="BluetoothFlossTelephony:disabled"/>
   <int value="846951416" label="CopylessPaste:enabled"/>
+  <int value="847000540" label="HoldingSpaceWallpaperNudge:disabled"/>
   <int value="848005486" label="WebViewLegacyTlsSupport:disabled"/>
   <int value="848324390" label="enable-lock-screen-apps"/>
   <int value="849235409" label="Prerender2InNewTab:enabled"/>
diff --git a/ui/aura/gestures/gesture_recognizer_unittest.cc b/ui/aura/gestures/gesture_recognizer_unittest.cc
index c319b27..f8e2c8d 100644
--- a/ui/aura/gestures/gesture_recognizer_unittest.cc
+++ b/ui/aura/gestures/gesture_recognizer_unittest.cc
@@ -4343,7 +4343,7 @@
   ui::TouchEvent move1(
       ui::ET_TOUCH_MOVED, gfx::Point(397, 149), tes.LeapForward(50),
       ui::PointerDetails(ui::EventPointerType::kTouch, kTouchId));
-  move1.set_flags(992);
+  move1.SetFlags(992);
 
   DispatchEventUsingWindowDispatcher(&move1);
   EXPECT_NE(default_flags, delegate->flags());
diff --git a/ui/aura/window_event_dispatcher.cc b/ui/aura/window_event_dispatcher.cc
index a63e34b..ab5f160 100644
--- a/ui/aura/window_event_dispatcher.cc
+++ b/ui/aura/window_event_dispatcher.cc
@@ -899,7 +899,7 @@
   int flags = event->flags();
   if (IsNonClientLocation(target, event->location()))
     flags |= ui::EF_IS_NON_CLIENT;
-  event->set_flags(flags);
+  event->SetFlags(flags);
 
   if (!is_dispatched_held_event(*event) &&
       (event->IsMouseEvent() || event->IsScrollEvent()) &&
diff --git a/ui/events/ash/event_rewriter_ash.cc b/ui/events/ash/event_rewriter_ash.cc
index 807a6740..7f94e05 100644
--- a/ui/events/ash/event_rewriter_ash.cc
+++ b/ui/events/ash/event_rewriter_ash.cc
@@ -1469,7 +1469,7 @@
   if (sticky_keys_controller_) {
     KeyEvent tmp_event = key_event;
     tmp_event.set_key_code(state.key_code);
-    tmp_event.set_flags(state.flags);
+    tmp_event.SetFlags(state.flags);
     std::unique_ptr<Event> output_event;
     status = sticky_keys_controller_->RewriteEvent(tmp_event, &output_event);
     if (status == EVENT_REWRITE_REWRITTEN ||
@@ -1527,7 +1527,7 @@
   EventRewriteStatus status = EVENT_REWRITE_CONTINUE;
   if (sticky_keys_controller_) {
     MouseEvent tmp_event = mouse_event;
-    tmp_event.set_flags(flags);
+    tmp_event.SetFlags(flags);
     std::unique_ptr<Event> output_event;
     status = sticky_keys_controller_->RewriteEvent(tmp_event, &output_event);
     if (status == EVENT_REWRITE_REWRITTEN ||
@@ -1545,7 +1545,7 @@
   }
 
   std::unique_ptr<Event> rewritten_event = mouse_event.Clone();
-  rewritten_event->set_flags(flags);
+  rewritten_event->SetFlags(flags);
   if (changed_button != EF_NONE) {
     static_cast<MouseEvent*>(rewritten_event.get())
         ->set_changed_button_flags(changed_button);
@@ -1572,7 +1572,7 @@
 
   const int flags = RewriteLocatedEvent(wheel_event);
   MouseWheelEvent tmp_event = wheel_event;
-  tmp_event.set_flags(flags);
+  tmp_event.SetFlags(flags);
   return sticky_keys_controller_->RewriteEvent(tmp_event, continuation);
 }
 
@@ -1584,7 +1584,7 @@
     return SendEvent(continuation, &touch_event);
   }
   TouchEvent rewritten_touch_event(touch_event);
-  rewritten_touch_event.set_flags(flags);
+  rewritten_touch_event.SetFlags(flags);
   return SendEventFinally(continuation, &rewritten_touch_event);
 }
 
diff --git a/ui/events/event.cc b/ui/events/event.cc
index e636a2a1..b40b117 100644
--- a/ui/events/event.cc
+++ b/ui/events/event.cc
@@ -299,6 +299,11 @@
   result_ = static_cast<EventResult>(result_ | ER_CONSUMED | ER_SKIPPED);
 }
 
+void Event::SetFlags(int flags) {
+  flags_ = flags;
+  OnFlagsUpdated();
+}
+
 std::string Event::ToString() const {
   return base::StrCat(
       {GetName(), " time_stamp=",
@@ -589,7 +594,7 @@
       f |= EF_IS_TRIPLE_CLICK;
       break;
   }
-  set_flags(f);
+  SetFlags(f);
 }
 
 std::string MouseEvent::ToString() const {
@@ -942,7 +947,7 @@
   // Check if this is a key repeat. This must be called before initial flags
   // processing, e.g: NormalizeFlags(), to avoid issues like crbug.com/1069690.
   if (synthesize_key_repeat_enabled_ && IsRepeated(GetLastKeyEvent()))
-    set_flags(flags() | EF_IS_REPEAT);
+    SetFlags(flags() | EF_IS_REPEAT);
 
 #if BUILDFLAG(IS_LINUX)
   NormalizeFlags();
@@ -950,11 +955,11 @@
   // Only Windows has native character events.
   if (is_char_) {
     key_ = DomKey::FromCharacter(static_cast<int32_t>(native_event().wParam));
-    set_flags(PlatformKeyMap::ReplaceControlAndAltWithAltGraph(flags()));
+    SetFlags(PlatformKeyMap::ReplaceControlAndAltWithAltGraph(flags()));
   } else {
     int adjusted_flags = flags();
     key_ = PlatformKeyMap::DomKeyFromKeyboardCode(key_code(), &adjusted_flags);
-    set_flags(adjusted_flags);
+    SetFlags(adjusted_flags);
   }
 #endif
 }
@@ -1046,7 +1051,7 @@
 
   if (is_repeat) {
     last->set_time_stamp(time_stamp());
-    last->set_flags(last->flags() | EF_IS_REPEAT);
+    last->SetFlags(last->flags() | EF_IS_REPEAT);
     return true;
   }
 
@@ -1158,9 +1163,9 @@
       return;
   }
   if (type() == ET_KEY_PRESSED)
-    set_flags(flags() | mask);
+    SetFlags(flags() | mask);
   else
-    set_flags(flags() & ~mask);
+    SetFlags(flags() & ~mask);
 }
 
 std::string KeyEvent::ToString() const {
diff --git a/ui/events/event.h b/ui/events/event.h
index f05e45e6..135b2a3 100644
--- a/ui/events/event.h
+++ b/ui/events/event.h
@@ -88,10 +88,7 @@
 
   // This is only intended to be used externally by classes that are modifying
   // events in an EventRewriter.
-  void set_flags(int flags) {
-    flags_ = flags;
-    OnFlagsUpdated();
-  }
+  void SetFlags(int flags);
 
   EventTarget* target() const { return target_; }
   EventPhase phase() const { return phase_; }
@@ -478,7 +475,7 @@
         movement_(model.movement_),
         pointer_details_(model.pointer_details_) {
     SetType(type);
-    set_flags(flags);
+    SetFlags(flags);
   }
 
   // Note: Use the ctor for MouseWheelEvent if type is ET_MOUSEWHEEL.
@@ -516,7 +513,7 @@
     // TODO(eirage): convert this to builder pattern.
     void set_movement(const gfx::Vector2dF& movement) {
       event_->movement_ = movement;
-      event_->set_flags(event_->flags() | EF_UNADJUSTED_MOUSE);
+      event_->SetFlags(event_->flags() | EF_UNADJUSTED_MOUSE);
     }
 
    private:
diff --git a/ui/events/gestures/motion_event_aura_unittest.cc b/ui/events/gestures/motion_event_aura_unittest.cc
index 3228a4e1..69a08e6 100644
--- a/ui/events/gestures/motion_event_aura_unittest.cc
+++ b/ui/events/gestures/motion_event_aura_unittest.cc
@@ -446,12 +446,12 @@
   MotionEventAura event;
 
   TouchEvent press0 = TouchWithType(ET_TOUCH_PRESSED, ids[0]);
-  press0.set_flags(EF_CONTROL_DOWN);
+  press0.SetFlags(EF_CONTROL_DOWN);
   EXPECT_TRUE(event.OnTouch(press0));
   EXPECT_EQ(EF_CONTROL_DOWN, event.GetFlags());
 
   TouchEvent press1 = TouchWithType(ET_TOUCH_PRESSED, ids[1]);
-  press1.set_flags(EF_CONTROL_DOWN | EF_CAPS_LOCK_ON);
+  press1.SetFlags(EF_CONTROL_DOWN | EF_CAPS_LOCK_ON);
   EXPECT_TRUE(event.OnTouch(press1));
   EXPECT_EQ(EF_CONTROL_DOWN | EF_CAPS_LOCK_ON, event.GetFlags());
 }
diff --git a/ui/events/win/media_keyboard_hook_win.cc b/ui/events/win/media_keyboard_hook_win.cc
index 8436668..485710e 100644
--- a/ui/events/win/media_keyboard_hook_win.cc
+++ b/ui/events/win/media_keyboard_hook_win.cc
@@ -107,7 +107,7 @@
   std::unique_ptr<KeyEvent> key_event =
       std::make_unique<KeyEvent>(KeyEventFromMSG(msg));
   if (is_repeat)
-    key_event->set_flags(key_event->flags() | EF_IS_REPEAT);
+    key_event->SetFlags(key_event->flags() | EF_IS_REPEAT);
   ForwardCapturedKeyEvent(key_event.get());
 
   // If the event is handled, don't propagate to the OS.
diff --git a/ui/events/win/modifier_keyboard_hook_win.cc b/ui/events/win/modifier_keyboard_hook_win.cc
index de4d2c3..800585d5 100644
--- a/ui/events/win/modifier_keyboard_hook_win.cc
+++ b/ui/events/win/modifier_keyboard_hook_win.cc
@@ -272,7 +272,7 @@
   std::unique_ptr<KeyEvent> key_event =
       std::make_unique<KeyEvent>(KeyEventFromMSG(msg));
   if (is_repeat)
-    key_event->set_flags(key_event->flags() | EF_IS_REPEAT);
+    key_event->SetFlags(key_event->flags() | EF_IS_REPEAT);
   ForwardCapturedKeyEvent(key_event.get());
 
   return true;
diff --git a/ui/file_manager/file_manager/foreground/js/ui/file_tap_handler_unittest.js b/ui/file_manager/file_manager/foreground/js/ui/file_tap_handler_unittest.ts
similarity index 76%
rename from ui/file_manager/file_manager/foreground/js/ui/file_tap_handler_unittest.js
rename to ui/file_manager/file_manager/foreground/js/ui/file_tap_handler_unittest.ts
index 0915dd3d..6efc6ff 100644
--- a/ui/file_manager/file_manager/foreground/js/ui/file_tap_handler_unittest.js
+++ b/ui/file_manager/file_manager/foreground/js/ui/file_tap_handler_unittest.ts
@@ -6,38 +6,23 @@
 
 import {FileTapHandler, TapEvent} from './file_tap_handler.js';
 
-/** @type {!FileTapHandler} handler the handler. */
-let handler;
+let handler: FileTapHandler;
 
-/**
- * @type {!Element}
- */
-let dummyTarget;
+let dummyTarget: Element;
 
-/**
- * @type {!Array<!Object>}
- */
-let events;
+let events: Array<{index: number, eventType: TapEvent}>;
 
-/**
- * @type {function(!Event, number, !TapEvent):boolean}
- */
-// @ts-ignore: error TS6133: 'e' is declared but its value is never read.
-const handleTap = (e, index, eventType) => {
-  events.push({index: index, eventType: eventType});
-  return false;
-};
+const handleTap =
+    (_e: TouchEvent, index: number, eventType: TapEvent): boolean => {
+      events.push({index, eventType});
+      return false;
+    };
 
-/**
- * @param {number} identifier
- * @param {number} clientX
- * @param {number} clientY
- */
-function createTouch(identifier, clientX, clientY) {
+function createTouch(identifier: number, clientX: number, clientY: number) {
   return new Touch({
-    identifier: identifier,
-    clientX: clientX,
-    clientY: clientY,
+    identifier,
+    clientX,
+    clientY,
     target: dummyTarget,
   });
 }
@@ -70,11 +55,8 @@
 
   // A tap event should be emitted for a single tap.
   assertEquals(1, events.length);
-  // @ts-ignore: error TS2339: Property 'eventType' does not exist on type
-  // 'Object'.
-  assertEquals(TapEvent.TAP, events[0].eventType);
-  // @ts-ignore: error TS2339: Property 'index' does not exist on type 'Object'.
-  assertEquals(0, events[0].index);
+  assertEquals(TapEvent.TAP, events[0]!.eventType);
+  assertEquals(0, events[0]!.index);
 }
 
 export function testTapMoveTolerance() {
@@ -129,12 +111,8 @@
   });
   // A long press should be emitted if there was no movement.
   assertEquals(1, events.length);
-  // @ts-ignore: error TS2339: Property 'eventType' does not exist on type
-  // 'Object'.
-  assertEquals(TapEvent.LONG_PRESS, events[0].eventType);
-  // @ts-ignore: error TS2339: Property 'index' does not exist on type
-  // 'Object'.
-  assertEquals(0, events[0].index);
+  assertEquals(TapEvent.LONG_PRESS, events[0]!.eventType);
+  assertEquals(0, events[0]!.index);
   handler.handleTouchEvents(
       new TouchEvent('touchend', {
         cancelable: true,
@@ -145,12 +123,8 @@
       1, handleTap);
   // A long tap should be emitted if there was no movement.
   assertEquals(2, events.length);
-  // @ts-ignore: error TS2339: Property 'eventType' does not exist on type
-  // 'Object'.
-  assertEquals(TapEvent.LONG_TAP, events[1].eventType);
-  // @ts-ignore: error TS2339: Property 'index' does not exist on type
-  // 'Object'.
-  assertEquals(0, events[1].index);
+  assertEquals(TapEvent.LONG_TAP, events[1]!.eventType);
+  assertEquals(0, events[1]!.index);
 }
 
 export async function testLongTapMoveTolerance() {
@@ -244,11 +218,8 @@
 
   // A two-finger tap event should be emitted, allowing for slight movement.
   assertEquals(1, events.length);
-  // @ts-ignore: error TS2339: Property 'eventType' does not exist on type
-  // 'Object'.
-  assertEquals(TapEvent.TWO_FINGER_TAP, events[0].eventType);
-  // @ts-ignore: error TS2339: Property 'index' does not exist on type 'Object'.
-  assertEquals(0, events[0].index);
+  assertEquals(TapEvent.TWO_FINGER_TAP, events[0]!.eventType);
+  assertEquals(0, events[0]!.index);
 
   // case 2: Release the first touch point first.
   handler.handleTouchEvents(
@@ -286,9 +257,6 @@
 
   // A two-finger tap event should be emitted.
   assertEquals(2, events.length);
-  // @ts-ignore: error TS2339: Property 'eventType' does not exist on type
-  // 'Object'.
-  assertEquals(TapEvent.TWO_FINGER_TAP, events[1].eventType);
-  // @ts-ignore: error TS2339: Property 'index' does not exist on type 'Object'.
-  assertEquals(10, events[1].index);
+  assertEquals(TapEvent.TWO_FINGER_TAP, events[1]!.eventType);
+  assertEquals(10, events[1]!.index);
 }
diff --git a/ui/file_manager/file_names.gni b/ui/file_manager/file_names.gni
index e731ded..effb7fa8 100644
--- a/ui/file_manager/file_names.gni
+++ b/ui/file_manager/file_names.gni
@@ -534,6 +534,7 @@
   "file_manager/foreground/js/ui/file_manager_dialog_base_unittest.ts",
   "file_manager/foreground/js/ui/file_table_list_unittest.ts",
   "file_manager/foreground/js/ui/file_table_unittest.ts",
+  "file_manager/foreground/js/ui/file_tap_handler_unittest.ts",
   "file_manager/foreground/js/ui/grid_unittest.ts",
   "file_manager/foreground/js/ui/install_linux_package_dialog_unittest.ts",
   "file_manager/foreground/js/ui/menu_unittest.ts",
@@ -627,7 +628,6 @@
 # Test files:
 unittest_files = [
   # Foreground:
-  "file_manager/foreground/js/ui/file_tap_handler_unittest.js",
   "file_manager/foreground/js/ui/directory_tree_unittest.js",
   "file_manager/foreground/js/navigation_list_model_unittest.js",
 
diff --git a/ui/platform_window/win/win_window.cc b/ui/platform_window/win/win_window.cc
index 605e36b..fad36c24 100644
--- a/ui/platform_window/win/win_window.cc
+++ b/ui/platform_window/win/win_window.cc
@@ -257,7 +257,7 @@
                     {CR_GET_X_LPARAM(l_param), CR_GET_Y_LPARAM(l_param)}};
   std::unique_ptr<Event> event = EventFromNative(msg);
   if (IsMouseEventFromTouch(message))
-    event->set_flags(event->flags() | EF_FROM_TOUCH);
+    event->SetFlags(event->flags() | EF_FROM_TOUCH);
   if (!(event->flags() & ui::EF_IS_NON_CLIENT))
     delegate_->DispatchEvent(event.get());
   SetMsgHandled(event->handled());
diff --git a/ui/views/controls/textfield/textfield_unittest.cc b/ui/views/controls/textfield/textfield_unittest.cc
index 269eb44..345a78e 100644
--- a/ui/views/controls/textfield/textfield_unittest.cc
+++ b/ui/views/controls/textfield/textfield_unittest.cc
@@ -3437,7 +3437,7 @@
   ui::MouseEvent press_2(ui::ET_MOUSE_PRESSED, point_2, point_2,
                          ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
                          ui::EF_LEFT_MOUSE_BUTTON);
-  press_2.set_flags(press_2.flags() | ui::EF_SHIFT_DOWN);
+  press_2.SetFlags(press_2.flags() | ui::EF_SHIFT_DOWN);
   textfield_->OnMousePressed(press_2);
   ui::MouseEvent release_2(ui::ET_MOUSE_RELEASED, point_2, point_2,
                            ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
diff --git a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc
index 2e69d7e0..a5c8ab5 100644
--- a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc
+++ b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc
@@ -364,7 +364,7 @@
   auto event = std::make_unique<ui::DropTargetEvent>(
       *data_to_drop_, target_location, gfx::PointF(root_location),
       last_drop_operation_);
-  event->set_flags(modifiers);
+  event->SetFlags(modifiers);
   if (delegate_has_changed)
     drag_drop_delegate_->OnDragEntered(*event);
   return event;
diff --git a/ui/views/widget/desktop_aura/desktop_drop_target_win.cc b/ui/views/widget/desktop_aura/desktop_drop_target_win.cc
index 0d13d59..bcabe6e 100644
--- a/ui/views/widget/desktop_aura/desktop_drop_target_win.cc
+++ b/ui/views/widget/desktop_aura/desktop_drop_target_win.cc
@@ -151,7 +151,7 @@
   *event = std::make_unique<ui::DropTargetEvent>(
       *(data->get()), gfx::PointF(location), gfx::PointF(root_location),
       ui::DragDropTypes::DropEffectToDragOperation(effect));
-  (*event)->set_flags(ConvertKeyStateToAuraEventFlags(key_state));
+  (*event)->SetFlags(ConvertKeyStateToAuraEventFlags(key_state));
   if (target_window_changed)
     (*delegate)->OnDragEntered(*event->get());
 }
diff --git a/ui/views/widget/desktop_aura/desktop_window_tree_host_linux.cc b/ui/views/widget/desktop_aura/desktop_window_tree_host_linux.cc
index 9dbf82a..cb1601b 100644
--- a/ui/views/widget/desktop_aura/desktop_window_tree_host_linux.cc
+++ b/ui/views/widget/desktop_aura/desktop_window_tree_host_linux.cc
@@ -204,7 +204,7 @@
           gfx::ToRoundedPoint(location_in_dip));
       if (hit_test_code != HTCLIENT && hit_test_code != HTNOWHERE)
         flags |= ui::EF_IS_NON_CLIENT;
-      located_event->set_flags(flags);
+      located_event->SetFlags(flags);
     }
 
     // While we unset the urgency hint when we gain focus, we also must remove
diff --git a/ui/views/widget/root_view.cc b/ui/views/widget/root_view.cc
index 9478b2d4..1b398ba 100644
--- a/ui/views/widget/root_view.cc
+++ b/ui/views/widget/root_view.cc
@@ -481,7 +481,7 @@
     // Remove the double-click flag if the handler is different than the
     // one which got the first click part of the double-click.
     if (mouse_pressed_handler_ != last_click_handler_)
-      mouse_pressed_event.set_flags(event.flags() & ~ui::EF_IS_DOUBLE_CLICK);
+      mouse_pressed_event.SetFlags(event.flags() & ~ui::EF_IS_DOUBLE_CLICK);
 
     drag_info_.Reset();
     ui::EventDispatchDetails dispatch_details =
diff --git a/ui/views/win/hwnd_message_handler.cc b/ui/views/win/hwnd_message_handler.cc
index 8044dca..36a6e980 100644
--- a/ui/views/win/hwnd_message_handler.cc
+++ b/ui/views/win/hwnd_message_handler.cc
@@ -3191,7 +3191,7 @@
                     {CR_GET_X_LPARAM(l_param), CR_GET_Y_LPARAM(l_param)}};
   ui::MouseEvent event(msg);
   if (IsSynthesizedMouseMessage(message, message_time, l_param))
-    event.set_flags(event.flags() | ui::EF_FROM_TOUCH);
+    event.SetFlags(event.flags() | ui::EF_FROM_TOUCH);
 
   if (event.type() == ui::ET_MOUSE_MOVED && !HasCapture() && track_mouse) {
     // Windows only fires WM_MOUSELEAVE events if the application begins
@@ -3500,7 +3500,7 @@
   ui::TouchEvent event(event_type, point, time_stamp,
                        ui::PointerDetails(ui::EventPointerType::kTouch, id));
 
-  event.set_flags(ui::GetModifiersFromKeyState());
+  event.SetFlags(ui::GetModifiersFromKeyState());
 
   event.latency()->AddLatencyNumberWithTimestamp(
       ui::INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT, time_stamp);
diff --git a/ui/webui/resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.ts b/ui/webui/resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.ts
index 89b7d92..99359428 100644
--- a/ui/webui/resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.ts
+++ b/ui/webui/resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.ts
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 
 import {assert} from '//resources/js/assert.js';
-import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {CustomElement} from '//resources/js/custom_element.js';
 
 import {getTemplate} from './cr_a11y_announcer.html.js';
 
@@ -50,20 +50,19 @@
   return instance;
 }
 
-export class CrA11yAnnouncerElement extends PolymerElement {
+export class CrA11yAnnouncerElement extends CustomElement {
   static get is() {
     return 'cr-a11y-announcer';
   }
 
-  static get template() {
+  static override get template() {
     return getTemplate();
   }
 
   private currentTimeout_: number|null = null;
   private messages_: string[] = [];
 
-  override disconnectedCallback() {
-    super.disconnectedCallback();
+  disconnectedCallback() {
     if (this.currentTimeout_ !== null) {
       clearTimeout(this.currentTimeout_);
       this.currentTimeout_ = null;
diff --git a/ui/wm/core/compound_event_filter_unittest.cc b/ui/wm/core/compound_event_filter_unittest.cc
index 4feb7a7..de4cfed 100644
--- a/ui/wm/core/compound_event_filter_unittest.cc
+++ b/ui/wm/core/compound_event_filter_unittest.cc
@@ -74,13 +74,13 @@
   // Synthesized mouse event should not show the cursor.
   ui::MouseEvent enter(ui::ET_MOUSE_ENTERED, gfx::Point(10, 10),
                        gfx::Point(10, 10), ui::EventTimeForNow(), 0, 0);
-  enter.set_flags(enter.flags() | ui::EF_IS_SYNTHESIZED);
+  enter.SetFlags(enter.flags() | ui::EF_IS_SYNTHESIZED);
   DispatchEventUsingWindowDispatcher(&enter);
   EXPECT_FALSE(cursor_client.IsCursorVisible());
 
   ui::MouseEvent move(ui::ET_MOUSE_MOVED, gfx::Point(10, 10),
                       gfx::Point(10, 10), ui::EventTimeForNow(), 0, 0);
-  move.set_flags(enter.flags() | ui::EF_IS_SYNTHESIZED);
+  move.SetFlags(enter.flags() | ui::EF_IS_SYNTHESIZED);
   DispatchEventUsingWindowDispatcher(&move);
   EXPECT_FALSE(cursor_client.IsCursorVisible());
 
@@ -107,7 +107,7 @@
   // Mouse synthesized exit event should not show the cursor.
   ui::MouseEvent exit(ui::ET_MOUSE_EXITED, gfx::Point(10, 10),
                       gfx::Point(10, 10), ui::EventTimeForNow(), 0, 0);
-  exit.set_flags(enter.flags() | ui::EF_IS_SYNTHESIZED);
+  exit.SetFlags(enter.flags() | ui::EF_IS_SYNTHESIZED);
   DispatchEventUsingWindowDispatcher(&exit);
   EXPECT_FALSE(cursor_client.IsCursorVisible());
 
@@ -245,12 +245,12 @@
 
   ui::MouseEvent mouse0(ui::ET_MOUSE_MOVED, gfx::Point(10, 10),
                         gfx::Point(10, 10), ui::EventTimeForNow(), 0, 0);
-  mouse0.set_flags(mouse0.flags() | ui::EF_FROM_TOUCH);
+  mouse0.SetFlags(mouse0.flags() | ui::EF_FROM_TOUCH);
 
   DispatchEventUsingWindowDispatcher(&mouse0);
   EXPECT_FALSE(cursor_client.IsMouseEventsEnabled());
 
-  mouse0.set_flags(mouse0.flags() & ~ui::EF_FROM_TOUCH);
+  mouse0.SetFlags(mouse0.flags() & ~ui::EF_FROM_TOUCH);
   DispatchEventUsingWindowDispatcher(&mouse0);
   EXPECT_TRUE(cursor_client.IsMouseEventsEnabled());