瀏覽代碼

Refactor Wagtail userbar as a web component (#9816)

* Add a border around the userbar menu in Windows high-contrast mode so it can be identified
* Make sure browser font resizing applies to the userbar
* Switch userbar to initialise a Web Component to avoid styling clashes
* Refactor userbar stylesheets to use the same CSS loading as the rest of the admin
Albina 2 年之前
父節點
當前提交
5cf621660c

+ 4 - 0
CHANGELOG.txt

@@ -51,6 +51,8 @@ Changelog
  * Fix: Fix horizontal positioning of rich text inline toolbar (Thibaud Colas)
  * Fix: Ensure that `DecimalBlock` correctly handles `None`, when `required=False`, values (Natarajan Balaji)
  * Fix: Close the userbar when clicking its toggle (Albina Starykova)
+ * Fix: Add a border around the userbar menu in Windows high-contrast mode so it can be identified (Albina Starykova)
+ * Fix: Make sure browser font resizing applies to the userbar (Albina Starykova)
  * Docs: Add custom permissions section to permissions documentation page (Dan Hayden)
  * Docs: Add documentation for how to get started with contributing translations for the Wagtail admin (Ogunbanjo Oluwadamilare)
  * Docs: Officially recommend `fnm` over `nvm` in development documentation (LB (Ben) Johnston)
@@ -89,6 +91,8 @@ Changelog
  * Maintenance: Update `tsconfig` to better support modern TypeScript development and clean up some code quality issues via Eslint (Loveth Omokaro)
  * Maintenance: Set up Stimulus application initialisation according to RFC 78 (LB (Ben) Johnston)
  * Maintenance: Refactor submit-on-change search filters for image and document listings to use Stimulus (LB (Ben) Johnston)
+ * Maintenance: Switch userbar to initialise a Web Component to avoid styling clashes (Albina Starykova)
+ * Maintenance: Refactor userbar stylesheets to use the same CSS loading as the rest of the admin (Albina Starykova)
 
 
 4.1.2 (xx.xx.xxxx) - IN DEVELOPMENT

+ 209 - 195
client/src/entrypoints/admin/userbar.js

@@ -5,154 +5,166 @@
 // This component implements a roving tab index for keyboard navigation
 // Learn more about roving tabIndex: https://w3c.github.io/aria-practices/#kbd_roving_tabindex
 
-document.addEventListener('DOMContentLoaded', () => {
-  const userbar = document.querySelector('[data-wagtail-userbar]');
-  const trigger = userbar.querySelector('[data-wagtail-userbar-trigger]');
-  const list = userbar.querySelector('[role=menu]');
-  const listItems = list.querySelectorAll('li');
-  const isActiveClass = 'is-active';
-
-  // querySelector for all items that can be focused
-  // tabIndex has been removed for roving tabindex compatibility
-  // source: https://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus
-  const focusableItemSelector = `a[href],
-    button:not([disabled]),
-    input:not([disabled])`;
+class Userbar extends HTMLElement {
+  connectedCallback() {
+    const template = document.getElementById('wagtail-userbar-template');
+    const shadowRoot = this.attachShadow({
+      mode: 'open',
+    });
+    shadowRoot.appendChild(template.content.cloneNode(true));
+    // Removes the template from html after it's being used
+    template.remove();
 
-  // eslint-disable-next-line @typescript-eslint/no-use-before-define
-  trigger.addEventListener('click', toggleUserbar, false);
+    const userbar = shadowRoot.querySelector('[data-wagtail-userbar]');
+    const trigger = userbar.querySelector('[data-wagtail-userbar-trigger]');
+    const list = userbar.querySelector('[role=menu]');
+    const listItems = list.querySelectorAll('li');
+    const isActiveClass = 'is-active';
 
-  // make sure userbar is hidden when navigating back
-  // eslint-disable-next-line @typescript-eslint/no-use-before-define
-  window.addEventListener('pageshow', hideUserbar, false);
+    // Avoid Web Component FOUC while stylesheets are loading.
+    userbar.style.display = 'none';
 
-  // Handle keyboard events on the trigger
-  // eslint-disable-next-line @typescript-eslint/no-use-before-define
-  userbar.addEventListener('keydown', handleTriggerKeyDown);
-  // eslint-disable-next-line @typescript-eslint/no-use-before-define
-  list.addEventListener('focusout', handleFocusChange);
+    // querySelector for all items that can be focused
+    // tabIndex has been removed for roving tabindex compatibility
+    // source: https://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus
+    const focusableItemSelector = `a[href],
+    button:not([disabled]),
+    input:not([disabled])`;
 
-  // eslint-disable-next-line @typescript-eslint/no-use-before-define
-  resetItemsTabIndex(); // On initialisation, all menu items should be disabled for roving tab index
+    // eslint-disable-next-line @typescript-eslint/no-use-before-define
+    trigger.addEventListener('click', toggleUserbar, false);
 
-  function showUserbar(shouldFocus) {
-    userbar.classList.add(isActiveClass);
-    trigger.setAttribute('aria-expanded', 'true');
+    // make sure userbar is hidden when navigating back
     // eslint-disable-next-line @typescript-eslint/no-use-before-define
-    list.addEventListener('click', sandboxClick, false);
+    window.addEventListener('pageshow', hideUserbar, false);
+
+    // Handle keyboard events on the trigger
+    // eslint-disable-next-line @typescript-eslint/no-use-before-define
+    userbar.addEventListener('keydown', handleTriggerKeyDown);
     // eslint-disable-next-line @typescript-eslint/no-use-before-define
-    window.addEventListener('click', clickOutside, false);
+    list.addEventListener('focusout', handleFocusChange);
 
-    // Start handling keyboard input now that the userbar is open.
     // eslint-disable-next-line @typescript-eslint/no-use-before-define
-    userbar.addEventListener('keydown', handleUserbarItemsKeyDown, false);
-
-    // The userbar has role=menu which means that the first link should be focused on popup
-    // For weird reasons shifting focus only works after some amount of delay
-    // Which is why we are forced to use setTimeout
-    if (shouldFocus) {
-      // Find the first focusable element (if any) and focus it
-      if (list.querySelector(focusableItemSelector)) {
-        setTimeout(() => {
-          // eslint-disable-next-line @typescript-eslint/no-use-before-define
-          setFocusToFirstItem();
-        }, 300); // Less than 300ms doesn't seem to work
+    resetItemsTabIndex(); // On initialisation, all menu items should be disabled for roving tab index
+
+    function showUserbar(shouldFocus) {
+      userbar.classList.add(isActiveClass);
+      trigger.setAttribute('aria-expanded', 'true');
+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      list.addEventListener('click', sandboxClick, false);
+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      window.addEventListener('click', clickOutside, false);
+
+      // Start handling keyboard input now that the userbar is open.
+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      userbar.addEventListener('keydown', handleUserbarItemsKeyDown, false);
+
+      // The userbar has role=menu which means that the first link should be focused on popup
+      // For weird reasons shifting focus only works after some amount of delay
+      // Which is why we are forced to use setTimeout
+      if (shouldFocus) {
+        // Find the first focusable element (if any) and focus it
+        if (list.querySelector(focusableItemSelector)) {
+          setTimeout(() => {
+            // eslint-disable-next-line @typescript-eslint/no-use-before-define
+            setFocusToFirstItem();
+          }, 300); // Less than 300ms doesn't seem to work
+        }
       }
     }
-  }
 
-  function hideUserbar() {
-    userbar.classList.remove(isActiveClass);
-    trigger.setAttribute('aria-expanded', 'false');
-    // eslint-disable-next-line @typescript-eslint/no-use-before-define
-    list.addEventListener('click', sandboxClick, false);
-    // eslint-disable-next-line @typescript-eslint/no-use-before-define
-    window.removeEventListener('click', clickOutside, false);
+    function hideUserbar() {
+      userbar.classList.remove(isActiveClass);
+      trigger.setAttribute('aria-expanded', 'false');
+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      list.addEventListener('click', sandboxClick, false);
+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      window.removeEventListener('click', clickOutside, false);
 
-    // Cease handling keyboard input now that the userbar is closed.
-    // eslint-disable-next-line @typescript-eslint/no-use-before-define
-    userbar.removeEventListener('keydown', handleUserbarItemsKeyDown, false);
-  }
+      // Cease handling keyboard input now that the userbar is closed.
+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      userbar.removeEventListener('keydown', handleUserbarItemsKeyDown, false);
+    }
 
-  function toggleUserbar(e2) {
-    e2.stopPropagation();
-    if (userbar.classList.contains(isActiveClass)) {
-      hideUserbar();
-    } else {
-      showUserbar(true);
+    function toggleUserbar(e2) {
+      e2.stopPropagation();
+      if (userbar.classList.contains(isActiveClass)) {
+        hideUserbar();
+      } else {
+        showUserbar(true);
+      }
     }
-  }
 
-  function isFocusOnItems() {
-    return (
-      document.activeElement &&
-      !!document.activeElement.closest('.wagtail-userbar-items')
-    );
-  }
+    function isFocusOnItems() {
+      return (
+        shadowRoot.activeElement &&
+        !!shadowRoot.activeElement.closest('.w-userbar-nav')
+      );
+    }
 
-  /** Reset all focusable menu items to `tabIndex = -1` */
-  function resetItemsTabIndex() {
-    listItems.forEach((listItem) => {
-      // eslint-disable-next-line no-param-reassign
-      listItem.firstElementChild.tabIndex = -1;
-    });
-  }
+    /** Reset all focusable menu items to `tabIndex = -1` */
+    function resetItemsTabIndex() {
+      listItems.forEach((listItem) => {
+        // eslint-disable-next-line no-param-reassign
+        listItem.firstElementChild.tabIndex = -1;
+      });
+    }
 
-  /** Focus element using a roving tab index */
-  function focusElement(el) {
-    resetItemsTabIndex();
-    // eslint-disable-next-line no-param-reassign
-    el.tabIndex = 0;
-    setTimeout(() => {
-      el.focus();
-    }, 100); // Workaround, changing focus only works after a timeout
-  }
+    /** Focus element using a roving tab index */
+    function focusElement(el) {
+      resetItemsTabIndex();
+      // eslint-disable-next-line no-param-reassign
+      el.tabIndex = 0;
+      setTimeout(() => {
+        el.focus();
+      }, 100); // Workaround, changing focus only works after a timeout
+    }
 
-  function setFocusToTrigger() {
-    setTimeout(() => trigger.focus(), 300);
-    resetItemsTabIndex();
-  }
+    function setFocusToTrigger() {
+      setTimeout(() => trigger.focus(), 300);
+      resetItemsTabIndex();
+    }
 
-  function setFocusToFirstItem() {
-    if (listItems.length > 0) {
-      focusElement(listItems[0].firstElementChild);
+    function setFocusToFirstItem() {
+      if (listItems.length > 0) {
+        focusElement(listItems[0].firstElementChild);
+      }
     }
-  }
 
-  function setFocusToLastItem() {
-    if (listItems.length > 0) {
-      focusElement(listItems[listItems.length - 1].firstElementChild);
+    function setFocusToLastItem() {
+      if (listItems.length > 0) {
+        focusElement(listItems[listItems.length - 1].firstElementChild);
+      }
     }
-  }
 
-  function setFocusToNextItem() {
-    listItems.forEach((element, idx) => {
-      // Check which item is currently focused
-      if (element.firstElementChild === document.activeElement) {
-        if (idx + 1 < listItems.length) {
-          focusElement(listItems[idx + 1].firstElementChild);
-        } else {
-          // Loop around
-          setFocusToFirstItem();
+    function setFocusToNextItem() {
+      listItems.forEach((element, idx) => {
+        // Check which item is currently focused
+        if (element.firstElementChild === shadowRoot.activeElement) {
+          if (idx + 1 < listItems.length) {
+            focusElement(listItems[idx + 1].firstElementChild);
+          } else {
+            // Loop around
+            setFocusToFirstItem();
+          }
         }
-      }
-    });
-  }
+      });
+    }
 
-  function setFocusToPreviousItem() {
-    listItems.forEach((element, idx) => {
-      // Check which item is currently focused
-      if (element.firstElementChild === document.activeElement) {
-        if (idx > 0) {
-          focusElement(listItems[idx - 1].firstElementChild);
-        } else {
-          setFocusToLastItem();
+    function setFocusToPreviousItem() {
+      listItems.forEach((element, idx) => {
+        // Check which item is currently focused
+        if (element.firstElementChild === shadowRoot.activeElement) {
+          if (idx > 0) {
+            focusElement(listItems[idx - 1].firstElementChild);
+          } else {
+            setFocusToLastItem();
+          }
         }
-      }
-    });
-  }
+      });
+    }
 
-  /**
+    /**
     This handler is responsible for keyboard input when items inside the userbar are focused.
     It should only listen when the userbar is open.
 
@@ -160,94 +172,96 @@ document.addEventListener('DOMContentLoaded', () => {
     - Shifting focus using the arrow / home / end keys.
     - Closing the menu when 'Escape' is pressed.
   */
-  function handleUserbarItemsKeyDown(event) {
-    // Only handle keyboard input if the userbar is open
-    if (trigger.getAttribute('aria-expanded') === 'true') {
-      if (event.key === 'Escape') {
-        hideUserbar();
-        setFocusToTrigger();
-        return false;
+    function handleUserbarItemsKeyDown(event) {
+      // Only handle keyboard input if the userbar is open
+      if (trigger.getAttribute('aria-expanded') === 'true') {
+        if (event.key === 'Escape') {
+          hideUserbar();
+          setFocusToTrigger();
+          return false;
+        }
+
+        // List items are in focus, move focus if needed
+        if (isFocusOnItems()) {
+          switch (event.key) {
+            case 'ArrowDown':
+              event.preventDefault();
+              setFocusToNextItem();
+              return false;
+            case 'ArrowUp':
+              event.preventDefault();
+              setFocusToPreviousItem();
+              return false;
+            case 'Home':
+              event.preventDefault();
+              setFocusToFirstItem();
+              return false;
+            case 'End':
+              event.preventDefault();
+              setFocusToLastItem();
+              return false;
+            default:
+              break;
+          }
+        }
       }
+      return true;
+    }
+
+    function handleFocusChange(event) {
+      // Is the focus is still in the menu? If so, don't to anything
+      if (
+        event.relatedTarget == null ||
+        (event.relatedTarget && event.relatedTarget.closest('.w-userbar-nav'))
+      ) {
+        return;
+      }
+      // List items not in focus - the menu should close
+      resetItemsTabIndex();
+      hideUserbar();
+    }
 
-      // List items are in focus, move focus if needed
-      if (isFocusOnItems()) {
+    /**
+    This handler is responsible for opening the userbar with the arrow keys
+    if it's focused and not open yet. It should always be listening.
+  */
+    function handleTriggerKeyDown(event) {
+      // Check if the userbar is focused (but not open yet) and should be opened by keyboard input
+      if (
+        trigger === document.activeElement &&
+        trigger.getAttribute('aria-expanded') === 'false'
+      ) {
         switch (event.key) {
-          case 'ArrowDown':
-            event.preventDefault();
-            setFocusToNextItem();
-            return false;
           case 'ArrowUp':
             event.preventDefault();
-            setFocusToPreviousItem();
-            return false;
-          case 'Home':
-            event.preventDefault();
-            setFocusToFirstItem();
-            return false;
-          case 'End':
+            showUserbar(false);
+
+            // Workaround for focus bug
+            // Needs extra delay to account for the userbar open animation. Otherwise won't focus properly.
+            setTimeout(() => setFocusToLastItem(), 300);
+            break;
+          case 'ArrowDown':
             event.preventDefault();
-            setFocusToLastItem();
-            return false;
+            showUserbar(false);
+
+            // Workaround for focus bug
+            // Needs extra delay to account for the userbar open animation. Otherwise won't focus properly.
+            setTimeout(() => setFocusToFirstItem(), 300);
+            break;
           default:
             break;
         }
       }
     }
-    return true;
-  }
 
-  function handleFocusChange(event) {
-    // Is the focus is still in the menu? If so, don't to anything
-    if (
-      event.relatedTarget == null ||
-      (event.relatedTarget &&
-        event.relatedTarget.closest('.wagtail-userbar-nav'))
-    ) {
-      return;
-    }
-    // List items not in focus - the menu should close
-    resetItemsTabIndex();
-    hideUserbar();
-  }
-
-  /**
-    This handler is responsible for opening the userbar with the arrow keys
-    if it's focused and not open yet. It should always be listening.
-  */
-  function handleTriggerKeyDown(event) {
-    // Check if the userbar is focused (but not open yet) and should be opened by keyboard input
-    if (
-      trigger === document.activeElement &&
-      trigger.getAttribute('aria-expanded') === 'false'
-    ) {
-      switch (event.key) {
-        case 'ArrowUp':
-          event.preventDefault();
-          showUserbar(false);
-
-          // Workaround for focus bug
-          // Needs extra delay to account for the userbar open animation. Otherwise won't focus properly.
-          setTimeout(() => setFocusToLastItem(), 300);
-          break;
-        case 'ArrowDown':
-          event.preventDefault();
-          showUserbar(false);
-
-          // Workaround for focus bug
-          // Needs extra delay to account for the userbar open animation. Otherwise won't focus properly.
-          setTimeout(() => setFocusToFirstItem(), 300);
-          break;
-        default:
-          break;
-      }
+    function sandboxClick(e2) {
+      e2.stopPropagation();
     }
-  }
 
-  function sandboxClick(e2) {
-    e2.stopPropagation();
+    function clickOutside() {
+      hideUserbar();
+    }
   }
+}
 
-  function clickOutside() {
-    hideUserbar();
-  }
-});
+customElements.define('wagtail-userbar', Userbar);

+ 2 - 1
client/tailwind.config.js

@@ -146,7 +146,8 @@ module.exports = {
      */
     plugin(({ addBase }) => {
       addBase({
-        ':root': {
+        /** Support for web components */
+        ':root, :host': {
           '--w-font-sans': fontFamily.sans.join(', '),
           '--w-font-mono': fontFamily.mono.join(', '),
           ...generateColorVariables(colors),

+ 16 - 0
docs/releases/4.2.md

@@ -70,6 +70,8 @@ This feature was developed by Sage Abdullah.
  * Fix horizontal positioning of rich text inline toolbar (Thibaud Colas)
  * Ensure that `DecimalBlock` correctly handles `None`, when `required=False`, values (Natarajan Balaji)
  * Close the userbar when clicking its toggle (Albina Starykova)
+ * Add a border around the userbar menu in Windows high-contrast mode so it can be identified (Albina Starykova)
+ * Make sure browser font resizing applies to the userbar (Albina Starykova)
 
 ### Documentation
 
@@ -113,6 +115,8 @@ This feature was developed by Sage Abdullah.
  * Move `identity` JavaScript util into shared utils folder (LB (Ben Johnston))
  * Remove unnecessary declaration of function to determine URL query params, instead use `URLSearchParams` (Loveth Omokaro)
  * Update `tsconfig` to better support modern TypeScript development and clean up some code quality issues via Eslint (Loveth Omokaro)
+ * Switch userbar to initialise a Web Component to avoid styling clashes (Albina Starykova)
+ * Refactor userbar stylesheets to use the same CSS loading as the rest of the admin (Albina Starykova)
 
 ## Upgrade considerations
 
@@ -152,3 +156,15 @@ Python code that uses the `InlinePanel` panel type is not affected by this chang
 ### `WAGTAILADMIN_GLOBAL_PAGE_EDIT_LOCK` setting is now `WAGTAILADMIN_GLOBAL_EDIT_LOCK`
 
 The `WAGTAILADMIN_GLOBAL_PAGE_EDIT_LOCK` setting has been renamed to [`WAGTAILADMIN_GLOBAL_EDIT_LOCK`](wagtailadmin_global_edit_lock).
+
+### Wagtail userbar as a web component
+
+The [`wagtailuserbar`](wagtailuserbar_tag) template tag now initialises the userbar as a [Web Component](https://developer.mozilla.org/en-US/docs/Web/Web_Components), with a `wagtail-userbar` custom element using shadow DOM to apply styles without any collisions with the host page.
+
+For any site customising the position of the userbar, target the styles to `wagtail-userbar::part(userbar)` instead of `.wagtail-userbar`. For example:
+
+```css
+wagtail-userbar::part(userbar) {
+    bottom: 30px;
+}
+```

+ 4 - 5
docs/topics/writing_templates.md

@@ -223,7 +223,7 @@ This tag provides a contextual flyout menu for logged-in users. The menu gives e
 
 This tag may be used on standard Django views, without page object. The user bar will contain one item pointing to the admin.
 
-We recommend putting the tag near the top of the `<body>` element to allow keyboard users to reach it. You should consider putting the tag after any `[skip links](https://webaim.org/techniques/skipnav/)` but before the navigation and main content of your page.
+We recommend putting the tag near the top of the `<body>` element to allow keyboard users to reach it. You should consider putting the tag after any [skip links](https://webaim.org/techniques/skipnav/)` but before the navigation and main content of your page.
 
 ```html+django
 {% load wagtailuserbar %}
@@ -254,11 +254,10 @@ By default, the User Bar appears in the bottom right of the browser window, inse
 The userbar can be positioned where it works best with your design. Alternatively, you can position it with a CSS rule in your own CSS files, for example:
 
 ```css
-.wagtail-userbar {
-    top: 200px !important;
-    left: 10px !important;
+wagtail-userbar::part(userbar) {
+    bottom: 30px;
 }
-```
+``` 
 
 ## Varying output between preview and live
 

+ 60 - 65
wagtail/admin/static_src/wagtailadmin/scss/userbar.scss

@@ -2,6 +2,9 @@
 @use 'sass:math';
 @use 'sass:string';
 
+@tailwind base;
+@tailwind components;
+
 @import '../../../../../client/scss/settings';
 @import '../../../../../client/scss/tools';
 
@@ -12,15 +15,10 @@
 $size-home-button: 3.5em;
 $position: 2em;
 $width-arrow: 0.6em;
-$box-shadow-props: 0 0 1px 0 rgba(107, 214, 230, 1);
+$box-shadow-props: 0 0 1px 0 rgba(107, 214, 230, 1),
+  0 1px 10px 0 rgba(107, 214, 230, 0.7);
 $max-items: 12;
 $userbar-radius: 6px;
-$color-black: #000;
-$color-white: #fff;
-$color-grey-1: #262626;
-
-// Classnames will start with this parameter, eg .wagtail-
-$namespace: 'wagtail';
 
 // Possible positions for the userbar to exist in. These are set through the
 // {% wagtailuserbar 'bottom-left' %} template tag.
@@ -50,23 +48,16 @@ $positions: (
 // =============================================================================
 // Wagtail userbar proper
 // =============================================================================
-.#{$namespace}-userbar-reset {
-  all: initial;
-  // Copy our font sans variable so it can be used without Tailwind.
-  --w-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui,
-    Roboto, 'Helvetica Neue', Arial, sans-serif, Apple Color Emoji,
-    'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
-}
-
-.#{$namespace}-userbar {
+.w-userbar {
   position: fixed;
   z-index: 9999;
-  // stylelint-disable-next-line declaration-no-important
-  font-size: initial !important;
+  font-size: initial;
   line-height: initial;
   margin: 0;
   padding: 0;
-  display: block;
+  // Stop hiding the userbar once stylesheets are loaded.
+  // stylelint-disable-next-line declaration-no-important
+  display: block !important;
   border: 0;
   width: auto;
   height: auto;
@@ -77,34 +68,32 @@ $positions: (
 }
 
 @media print {
-  .#{$namespace}-userbar {
+  .w-userbar {
     display: none;
   }
 }
 
-// stylelint-disable declaration-no-important
-.#{$namespace}-userbar-trigger {
-  all: initial;
+.w-userbar-trigger {
   display: flex;
   align-items: center;
   justify-content: center;
   width: $size-home-button;
   height: $size-home-button;
-  margin: 0 !important;
+  margin: 0;
   overflow: hidden;
   background-color: $color-white;
   border: 2px solid transparent;
   border-radius: 50%;
   color: $color-black;
-  padding: 0 !important;
+  padding: 0;
   cursor: pointer;
-  box-shadow: $box-shadow-props, 0 1px 10px 0 rgba(107, 214, 230, 0.7);
+  box-shadow: $box-shadow-props;
   transition: all 0.2s ease-in-out;
-  font-size: 16px;
-  text-decoration: none !important;
+  font-size: 1rem;
+  text-decoration: none;
   position: relative;
 
-  .#{$namespace}-userbar-help-text {
+  .w-userbar-help-text {
     // Visually hide the help text
     clip: rect(0 0 0 0);
     clip-path: inset(50%);
@@ -115,9 +104,9 @@ $positions: (
     width: 1px;
   }
 
-  .#{$namespace}-icon:before {
+  .w-icon:before {
     transition: color 0.2s ease;
-    font-size: 32px;
+    font-size: 2rem;
     width: auto;
     margin: 0;
   }
@@ -127,8 +116,7 @@ $positions: (
   }
 }
 
-.#{$namespace}-userbar-items {
-  all: revert;
+.w-userbar-items {
   display: block;
   list-style: none;
   position: absolute;
@@ -136,17 +124,17 @@ $positions: (
   min-width: 210px;
   visibility: hidden;
   font-family: $font-sans;
-  font-size: 14px;
+  font-size: 0.875rem;
   padding-inline-start: 0;
   text-decoration: none;
 
-  .#{$namespace}-userbar.is-active & {
+  .w-userbar.is-active & {
     visibility: visible;
   }
 }
 
 // Arrow
-.#{$namespace}-userbar-items:after {
+.w-userbar-items:after {
   content: '';
   position: absolute;
   width: 0;
@@ -157,29 +145,28 @@ $positions: (
   transition-timing-function: cubic-bezier(0.55, 0, 0.1, 1);
 
   @media (prefers-reduced-motion: reduce) {
-    transition: none !important;
+    transition: none;
   }
 
-  .#{$namespace}-userbar.is-active & {
+  .w-userbar.is-active & {
     opacity: 1;
     transform: translateY(0);
     transition-delay: 0.3s;
   }
 }
 
-.#{$namespace}-userbar-nav {
-  background: transparent !important;
+.w-userbar-nav {
+  background: transparent;
   padding: 0;
-  margin: 0 !important;
-  display: block !important;
+  margin: 0;
+  display: block;
 
-  .#{$namespace}-action {
+  .w-action {
     background: transparent;
   }
 }
 
-.#{$namespace}-userbar__item {
-  all: revert;
+.w-userbar__item {
   margin: 0;
   background-color: $color-grey-1;
   opacity: 0;
@@ -187,14 +174,14 @@ $positions: (
   transition-duration: 0.125s;
   transition-timing-function: cubic-bezier(0.55, 0, 0.1, 1);
   font-family: $font-sans;
-  font-size: 16px !important;
-  text-decoration: none !important;
+  font-size: 1rem;
+  text-decoration: none;
 
   @media (prefers-reduced-motion: reduce) {
-    transition: none !important;
+    transition: none;
 
     // Force disable transitions for all items
-    transition-delay: 0s !important;
+    transition-delay: 0s;
   }
 
   &:first-child {
@@ -224,14 +211,14 @@ $positions: (
   }
 
   a,
-  .#{$namespace}-action {
+  .w-action {
     color: $color-white;
     display: block;
-    text-decoration: none !important;
-    transform: none !important;
-    transition: none !important;
-    margin: 0 !important;
-    font-size: 14px !important;
+    text-decoration: none;
+    transform: none;
+    transition: none;
+    margin: 0;
+    font-size: 0.875rem;
 
     &:hover,
     &:focus {
@@ -250,7 +237,7 @@ $positions: (
     }
   }
 
-  .#{$namespace}-icon {
+  .w-icon {
     position: relative;
 
     &:before {
@@ -263,7 +250,7 @@ $positions: (
 
   a,
   button {
-    font-size: 14px !important;
+    font-size: 0.875rem;
     text-align: start;
     padding: 0.8em;
   }
@@ -279,9 +266,17 @@ $positions: (
 //Media for Windows High Contrast
 
 @media (forced-colors: $media-forced-colours) {
-  .#{$namespace}-userbar-icon {
+  .w-userbar-icon {
     fill: $system-color-link-text;
   }
+
+  .w-userbar__item {
+    border: 1px solid $system-color-button-text;
+  }
+
+  .w-userbar-items::after {
+    border: $width-arrow solid Canvas;
+  }
 }
 
 // =============================================================================
@@ -293,17 +288,17 @@ $positions: (
   $horizontal: map.get($attrs, horizontal);
   $arrow: map.get($attrs, arrow);
 
-  .#{$namespace}-userbar--#{$pos} {
+  .w-userbar--#{$pos} {
     #{$vertical}: $position;
     #{$horizontal}: $position;
 
-    .#{$namespace}-userbar-items {
+    .w-userbar-items {
       #{$vertical}: 100%;
       #{$horizontal}: 0;
       padding-#{$vertical}: $width-arrow * 2;
     }
 
-    .#{$namespace}-userbar-nav .#{$namespace}-userbar__item {
+    .w-userbar-nav .w-userbar__item {
       @if $vertical == 'bottom' {
         transform: translateY(1em);
       } @else {
@@ -311,21 +306,21 @@ $positions: (
       }
     }
 
-    .#{$namespace}-userbar-items:after {
+    .w-userbar-items:after {
       #{$vertical}: 2px;
       #{$horizontal}: math.div($size-home-button, 2) -
         math.div($width-arrow, 2);
       border-#{$arrow}-color: $color-grey-1;
 
       @if $vertical == 'bottom' {
-        transform: translateY(-$width-arrow);
+        transform: translateY($width-arrow);
       }
       @if $vertical == 'top' {
         transform: translateY($width-arrow);
       }
     }
 
-    &.is-active .#{$namespace}-userbar__item {
+    &.is-active .w-userbar__item {
       @for $i from 1 through $max-items {
         @if $vertical == 'bottom' {
           &:nth-last-child(#{$i}) {
@@ -348,7 +343,7 @@ $positions: (
 // =============================================================================
 
 // Active state for the list items comes last.
-.#{$namespace}-userbar.is-active .#{$namespace}-userbar__item {
+.w-userbar.is-active .w-userbar__item {
   transform: translateY(0);
   opacity: 1;
 }

+ 10 - 9
wagtail/admin/templates/wagtailadmin/userbar/base.html

@@ -1,10 +1,10 @@
 {% load wagtailadmin_tags i18n %}
 <!-- Wagtail user bar embed code -->
-<div class="wagtail-userbar-reset">
-    <div class="wagtail-userbar wagtail-userbar--{{ position|default:'bottom-right' }}" data-wagtail-userbar>
+<template id="wagtail-userbar-template">
+    <div class="w-userbar w-userbar--{{ position|default:'bottom-right' }}" data-wagtail-userbar part="userbar">
         <link rel="stylesheet" href="{% versioned_static 'wagtailadmin/css/userbar.css' %}">
-        <div class="wagtail-userbar-nav">
-            <button aria-controls="wagtail-userbar-items" aria-haspopup="true" class="wagtail-userbar-trigger" id="wagtail-userbar-trigger" data-wagtail-userbar-trigger>
+        <div class="w-userbar-nav">
+            <button aria-controls="wagtail-userbar-items" aria-haspopup="true" class="w-userbar-trigger" id="wagtail-userbar-trigger" data-wagtail-userbar-trigger>
                 {% block branding_logo %}
                     <div style="display: none">
                         <svg>
@@ -19,19 +19,20 @@
                         </svg>
                     </div>
                     {% comment %} Intentionally not using the icon template tag to show as SVG only {% endcomment %}
-                    <svg class="wagtail-userbar-icon" aria-hidden="true">
+                    <svg class="w-userbar-icon" aria-hidden="true">
                         <use href="#icon-wagtail-icon"></use>
                     </svg>
                 {% endblock %}
-                <span class="wagtail-userbar-help-text">{% trans 'View Wagtail quick actions' %}</span>
+                <span class="w-userbar-help-text">{% trans 'View Wagtail quick actions' %}</span>
             </button>
-            <ul aria-labelledby="wagtail-userbar-trigger" class="wagtail-userbar-items" id="wagtail-userbar-items" role="menu">
+            <ul aria-labelledby="wagtail-userbar-trigger" class="w-userbar-items" id="wagtail-userbar-items" role="menu">
                 {% for item in items %}
                     {{ item|safe }}
                 {% endfor %}
             </ul>
         </div>
-        <script src="{% versioned_static 'wagtailadmin/js/userbar.js' %}"></script>
     </div>
-</div>
+</template>
+<wagtail-userbar></wagtail-userbar>
+<script src="{% versioned_static 'wagtailadmin/js/userbar.js' %}"></script>
 <!-- end Wagtail user bar embed code -->

+ 2 - 2
wagtail/admin/templates/wagtailadmin/userbar/item_admin.html

@@ -2,8 +2,8 @@
 {% load i18n wagtailadmin_tags %}
 
 {% block item_content %}
-    <a href="{% url 'wagtailadmin_home' %}" target="_parent" class="wagtail-userbar-link" role="menuitem">
-        {% icon name="wagtail-icon" class_name="wagtail-action-icon" %}
+    <a href="{% url 'wagtailadmin_home' %}" target="_parent" role="menuitem">
+        {% icon name="wagtail-icon" class_name="w-action-icon" %}
         {% trans 'Go to Wagtail admin' %}
     </a>
 {% endblock %}

+ 1 - 1
wagtail/admin/templates/wagtailadmin/userbar/item_base.html

@@ -1,3 +1,3 @@
-<li class="wagtail-userbar__item {% block item_classes %}{% endblock %}" role="presentation">
+<li class="w-userbar__item {% block item_classes %}{% endblock %}" role="presentation">
     {% block item_content %}{% endblock %}
 </li>

+ 1 - 1
wagtail/admin/templates/wagtailadmin/userbar/item_page_add.html

@@ -3,7 +3,7 @@
 
 {% block item_content %}
     <a href="{% url 'wagtailadmin_pages:add_subpage' self.page.id %}" target="_parent" role="menuitem">
-        {% icon name="plus" class_name="wagtail-action-icon" %}
+        {% icon name="plus" class_name="w-action-icon" %}
         {% trans 'Add a child page' %}
     </a>
 {% endblock %}

+ 1 - 1
wagtail/admin/templates/wagtailadmin/userbar/item_page_approve.html

@@ -5,7 +5,7 @@
     <form action="{% url 'wagtailadmin_pages:approve_moderation' self.revision.id %}" target="_parent" method="post">
         {% csrf_token %}
         <button type="submit" value="{% trans 'Approve' %}" class="button" role="menuitem">
-            {% icon name="tick" class_name="wagtail-action-icon" %}
+            {% icon name="tick" class_name="w-action-icon" %}
             {% trans 'Approve' %}
         </button>
     </form>

+ 1 - 1
wagtail/admin/templates/wagtailadmin/userbar/item_page_edit.html

@@ -3,7 +3,7 @@
 
 {% block item_content %}
     <a href="{% url 'wagtailadmin_pages:edit' self.page.id %}" target="_parent" role="menuitem">
-        {% icon name="edit" class_name="wagtail-action-icon" %}
+        {% icon name="edit" class_name="w-action-icon" %}
         {% trans 'Edit this page' %}
     </a>
 {% endblock %}

+ 1 - 1
wagtail/admin/templates/wagtailadmin/userbar/item_page_explore.html

@@ -3,7 +3,7 @@
 
 {% block item_content %}
     <a href="{% url 'wagtailadmin_explore' self.parent_page.id %}" target="_parent" role="menuitem">
-        {% icon name="folder-open-inverse" class_name="wagtail-action-icon" %}
+        {% icon name="folder-open-inverse" class_name="w-action-icon" %}
         {% trans 'Show in Explorer' %}
     </a>
 {% endblock %}

+ 1 - 1
wagtail/admin/templates/wagtailadmin/userbar/item_page_reject.html

@@ -5,7 +5,7 @@
     <form action="{% url 'wagtailadmin_pages:reject_moderation' self.revision.id %}" target="_parent" method="post">
         {% csrf_token %}
         <button type="submit" value="{% trans 'Reject' %}" class="button" role="menuitem">
-            {% icon name="cross" class_name="wagtail-action-icon" %}
+            {% icon name="cross" class_name="w-action-icon" %}
             {% trans 'Reject' %}
         </button>
     </form>

+ 3 - 3
wagtail/admin/tests/test_userbar.py

@@ -197,7 +197,7 @@ class TestUserbarAddLink(TestCase, WagtailTestUtils):
         )
         needle = f"""
             <a href="{expected_url}" target="_parent" role="menuitem">
-                <svg class="icon icon-plus wagtail-action-icon" aria-hidden="true">
+                <svg class="icon icon-plus w-action-icon" aria-hidden="true">
                     <use href="#icon-plus"></use>
                 </svg>
                 Add a child page
@@ -238,7 +238,7 @@ class TestUserbarModeration(TestCase, WagtailTestUtils):
         expected_approve_html = """
             <form action="/admin/pages/moderation/{}/approve/" target="_parent" method="post">
                 <input type="hidden" name="csrfmiddlewaretoken">
-                <div class="wagtail-action">
+                <div class="w-action">
                     <input type="submit" value="Approve" class="button" />
                 </div>
             </form>
@@ -250,7 +250,7 @@ class TestUserbarModeration(TestCase, WagtailTestUtils):
         expected_reject_html = """
             <form action="/admin/pages/moderation/{}/reject/" target="_parent" method="post">
                 <input type="hidden" name="csrfmiddlewaretoken">
-                <div class="wagtail-action">
+                <div class="w-action">
                     <input type="submit" value="Reject" class="button" />
                 </div>
             </form>