Prechádzať zdrojové kódy

Page editor underline tabs (#8266)

Co-authored-by: Thibaud Colas <thibaudcolas@gmail.com>
Steve Stein 2 rokov pred
rodič
commit
629ced01ca
50 zmenil súbory, kde vykonal 929 pridanie a 745 odobranie
  1. 5 1
      .eslintrc.js
  2. 2 0
      CHANGELOG.txt
  3. 0 23
      client/scss/components/_header.scss
  4. 0 14
      client/scss/components/_modals.scss
  5. 60 148
      client/scss/components/_tabs.scss
  6. 0 1
      client/scss/core.scss
  7. 5 0
      client/scss/elements/_elements.scss
  8. 0 25
      client/scss/overrides/_utilities.scrollbars.scss
  9. 1 1
      client/src/components/Sidebar/modules/MainMenu.tsx
  10. 1 1
      client/src/components/Sidebar/modules/__snapshots__/MainMenu.test.js.snap
  11. 3 1
      client/src/entrypoints/admin/comments.js
  12. 5 37
      client/src/entrypoints/admin/core.js
  13. 5 3
      client/src/entrypoints/admin/page-editor.js
  14. 7 9
      client/src/entrypoints/admin/task-chooser-modal.js
  15. 2 0
      client/src/entrypoints/admin/wagtailadmin.js
  16. 10 5
      client/src/entrypoints/documents/document-chooser-modal.js
  17. 10 3
      client/src/entrypoints/images/image-chooser-modal.js
  18. 1 0
      client/src/entrypoints/images/image-chooser.js
  19. 3 0
      client/src/includes/breadcrumbs.js
  20. 327 0
      client/src/includes/tabs.js
  21. 33 0
      client/src/plugins/scrollbarThin.js
  22. 2 1
      client/tailwind.config.js
  23. 10 2
      client/webpack.config.js
  24. 3 2
      docs/releases/3.0.md
  25. 0 138
      wagtail/admin/static_src/wagtailadmin/js/vendor/bootstrap-tab.js
  26. 68 50
      wagtail/admin/templates/wagtailadmin/account/account.html
  27. 37 26
      wagtail/admin/templates/wagtailadmin/panels/tabbed_interface.html
  28. 1 1
      wagtail/admin/templates/wagtailadmin/shared/breadcrumb-next.html
  29. 2 3
      wagtail/admin/templates/wagtailadmin/shared/header.html
  30. 19 0
      wagtail/admin/templates/wagtailadmin/shared/tabs/tab_nav_link.html
  31. 45 15
      wagtail/admin/templates/wagtailadmin/workflows/task_chooser/chooser.html
  32. 1 1
      wagtail/admin/templates/wagtailadmin/workflows/task_chooser/includes/results.html
  33. 9 9
      wagtail/admin/tests/pages/test_create_page.py
  34. 5 11
      wagtail/admin/tests/test_edit_handlers.py
  35. 1 1
      wagtail/contrib/modeladmin/templates/modeladmin/choose_parent.html
  36. 1 1
      wagtail/contrib/modeladmin/templates/modeladmin/create.html
  37. 1 1
      wagtail/contrib/modeladmin/templates/modeladmin/edit.html
  38. 1 1
      wagtail/contrib/modeladmin/templates/modeladmin/inspect.html
  39. 1 1
      wagtail/contrib/settings/templates/wagtailsettings/edit.html
  40. 12 17
      wagtail/contrib/styleguide/templates/wagtailstyleguide/base.html
  41. 37 25
      wagtail/documents/templates/wagtaildocs/chooser/chooser.html
  42. 1 2
      wagtail/documents/templates/wagtaildocs/chooser/results.html
  43. 7 1
      wagtail/documents/templates/wagtaildocs/chooser/upload_form.html
  44. 43 33
      wagtail/images/templates/wagtailimages/chooser/chooser.html
  45. 7 2
      wagtail/images/templates/wagtailimages/chooser/upload_form.html
  46. 1 1
      wagtail/snippets/templates/wagtailsnippets/snippets/create.html
  47. 1 1
      wagtail/snippets/templates/wagtailsnippets/snippets/edit.html
  48. 8 38
      wagtail/snippets/tests.py
  49. 55 37
      wagtail/users/templates/wagtailusers/users/create.html
  50. 70 52
      wagtail/users/templates/wagtailusers/users/edit.html

+ 5 - 1
.eslintrc.js

@@ -112,7 +112,11 @@ module.exports = {
       globals: { $: 'readonly' },
     },
     {
-      files: ['wagtail/**/**'],
+      files: [
+        'wagtail/**/**',
+        'client/src/entrypoints/documents/document-chooser-modal.js',
+        'client/src/entrypoints/images/image-chooser-modal.js',
+      ],
       globals: {
         $: 'readonly',
         addMessage: 'readonly',

+ 2 - 0
CHANGELOG.txt

@@ -49,6 +49,7 @@ Changelog
  * Add the ability for choices to be separated by new lines instead of just commas within the form builder, commas will still be supported if used (Abdulmajeed Isa)
  * Add internationalisation UI to modeladmin (Andrés Martano)
  * Support chunking in `PageQuerySet.specific()` to reduce memory consumption (Andy Babic)
+ * Implement new tabs design across the admin interface (Steven Steinwand)
  * Fix: When using `simple_translations` ensure that the user is redirected to the page edit view when submitting for a single locale (Mitchel Cabuloy)
  * Fix: When previewing unsaved changes to `Form` pages, ensure that all added fields are correctly shown in the preview (Joshua Munn)
  * Fix: When Documents (e.g. PDFs) have been configured to be served inline via `WAGTAILDOCS_CONTENT_TYPES` & `WAGTAILDOCS_INLINE_CONTENT_TYPES` ensure that the filename is correctly set in the `Content-Disposition` header so that saving the files will use the correct filename (John-Scott Atlakson)
@@ -68,6 +69,7 @@ Changelog
  * Fix: Page copy in Wagtail admin ignores `exclude_fields_in_copy` (John-Scott Atlakson)
  * Fix: Translation key `IntegrityError` when publishing pages with translatable `Orderable`s that were copied without being published (Kalob Taulien, Dan Braghis)
  * Fix: Ignore `GenericRelation` when copying pages (John-Scott Atlakson)
+ * Fix: Implement ARIA tabs markup and keyboards interactions for admin tabs (Steven Steinwand)
 
 
 2.16.2 (11.04.2022)

+ 0 - 23
client/scss/components/_header.scss

@@ -69,28 +69,6 @@ header {
     margin-bottom: 0;
   }
 
-  &.tab-merged {
-    padding-inline-start: $desktop-nice-padding;
-    padding-inline-end: $desktop-nice-padding;
-
-    .right:last-child {
-      padding-inline-end: 0;
-    }
-
-    @include media-breakpoint-down(xs) {
-      .breadcrumb {
-        padding-inline-start: calc(#{$desktop-nice-padding} - 8px);
-      }
-    }
-    @include media-breakpoint-up(sm) {
-      .breadcrumb {
-        margin-inline-start: -$desktop-nice-padding;
-        margin-inline-end: -$desktop-nice-padding;
-        padding-inline-start: math.div($desktop-nice-padding, 2);
-      }
-    }
-  }
-
   &.header-with-breadcrumb {
     padding-top: 0;
 
@@ -103,7 +81,6 @@ header {
     }
   }
 
-  &.tab-merged,
   &.no-border {
     border: 0;
 

+ 0 - 14
client/scss/components/_modals.scss

@@ -124,10 +124,6 @@ $zindex-modal-background: 500;
     h1 {
       @apply w-text-white;
     }
-
-    &.tab-merged {
-      padding-inline-start: 1.6em;
-    }
   }
 
   .header-title {
@@ -135,22 +131,12 @@ $zindex-modal-background: 500;
     padding-inline-start: 0 !important;
     margin-inline-start: -36px;
   }
-
-  .tab-merged .header-title {
-    margin-inline-start: 0;
-  }
 }
 
 @include media-breakpoint-up(sm) {
   .modal-dialog {
     padding: 0 0 2em $menu-width;
   }
-
-  .modal-body {
-    header.tab-merged {
-      padding-inline-start: $desktop-nice-padding;
-    }
-  }
 }
 
 @include media-breakpoint-up(xl) {

+ 60 - 148
client/scss/components/_tabs.scss

@@ -1,111 +1,4 @@
-.tab-nav {
-  @apply w-bg-grey-50;
-  @include row();
-  padding: 0;
-
-  li {
-    list-style-type: none;
-    width: 33%;
-    float: left;
-    padding: 0;
-    position: relative;
-    margin-inline-end: 2px;
-
-    &:first-of-type {
-      padding-inline-start: $desktop-nice-padding;
-      margin-inline-start: 0;
-    }
-  }
-
-  h2 {
-    margin: 0;
-    font-size: inherit;
-  }
-
-  a {
-    // border-top 0.3em is temporary until the new tab design is implemented
-    @apply w-bg-primary w-border-t-[0.3em] w-border-primary-200;
-    @include transition(border-color 0.2s ease);
-    font-weight: 600;
-    text-decoration: none;
-    display: block;
-    padding: 0.6em 0.7em 0.8em;
-    color: $color-white;
-    max-height: 1.44em;
-    overflow: hidden;
-
-    &:hover {
-      color: $color-white;
-      border-top-color: rgba(0, 0, 0, 0.35);
-    }
-  }
-
-  a.errors {
-    &:after {
-      border-radius: 50px;
-      box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.1);
-      position: absolute;
-      // Remove once we drop support for Safari 13.
-      // stylelint-disable-next-line property-disallowed-list
-      right: -0.5em;
-      inset-inline-end: -0.5em;
-      top: -0.5em;
-      z-index: 5;
-      min-width: 0.9em;
-      color: $color-white;
-      background: $color-red;
-      content: attr(data-count);
-      padding: 0 0.3em;
-      line-height: 1.4em;
-      text-align: center;
-      font-size: 0.8em;
-    }
-  }
-
-  li.active a {
-    box-shadow: none;
-    color: $color-grey-1;
-    background-color: $color-white;
-    border-top: 0.3em solid $color-grey-1;
-  }
-
-  // For cases where tab-nav should merge with header
-  .page-editor & {
-    &.merged {
-      @apply w-pt-2 sm:w-pt-4;
-    }
-  }
-
-  &.merged {
-    @apply w-mt-0;
-  }
-
-  li.right {
-    float: right;
-    margin-inline-end: 0;
-    margin-inline-start: 2px;
-  }
-
-  li.wide {
-    width: unset;
-  }
-
-  .right {
-    max-height: 1.44em;
-    overflow: visible;
-  }
-}
-
 .tab-content {
-  > section {
-    display: none;
-    padding-top: 1em;
-
-    &.active {
-      display: block;
-    }
-  }
-
   .page-locked & {
     cursor: not-allowed;
     user-select: none;
@@ -116,53 +9,72 @@
   }
 }
 
-@include media-breakpoint-up(sm) {
-  .tab-nav {
-    // For cases where tab-nav should merge with header
-    &.merged {
-      @apply w-bg-grey-50;
-    }
-
-    li {
-      width: auto;
-      padding: 0;
-    }
-
-    a {
-      padding-inline-start: $mobile-nice-padding;
-      padding-inline-end: $mobile-nice-padding;
-    }
-
-    li.settings a {
-      padding-inline-start: 2em;
-      padding-inline-end: 2em;
+.w-tabs {
+  &__wrapper {
+    @apply w-mb-10 w-overflow-x-auto w-scrollbar-thin;
+  }
+
+  &__list {
+    @include nice-padding();
+    @apply w-flex w-my-[3px] w-space-x-6 w-border-b w-border-grey-100 w-w-fit;
+  }
+
+  &__tab {
+    @apply w-label-3
+    w-box-border
+    w-inline-flex
+    w-text-grey-400
+    hover:w-text-primary
+    w-whitespace-nowrap
+    w-py-4
+    w-font-medium
+    w-relative
+    after:w-block
+    after:w-w-0
+    after:w-h-[2px]
+    after:w-bg-primary
+    after:w-absolute
+    after:w-left-0
+    after:-w-bottom-px
+    after:w-transition-all
+    motion-reduce:after:w-transition-none
+    hover:after:w-w-full;
+
+    &[aria-selected='true'] {
+      @apply after:w-w-full w-text-primary;
     }
   }
 
-  .modal-content .tab-nav li {
-    padding: 0;
-    min-width: 0;
-
-    &:first-of-type {
-      padding-inline-start: $desktop-nice-padding;
+  &__errors {
+    @apply w-hidden
+    w-box-border
+    w-w-4
+    w-h-4
+    w-text-[0.75rem]
+    w-flex
+    w-justify-center
+    w-items-center
+    w-font-bold
+    w-bg-critical-200
+    w-text-white
+    w-border
+    w-border-white
+    w-rounded-full
+    w-absolute
+    w-top-[0.4375rem]
+    -w-right-[0.9375rem];
+
+    &--active {
+      @apply w-flex;
     }
   }
-}
 
-@include media-breakpoint-down(xs) {
-  // To allow tabs on the edit page to be editable
-  .tab-nav li:first-of-type {
-    padding-inline-start: 1.6em;
-  }
+  // Optional animate attr for tabs to animate in
+  &[data-tabs-animate] &__panel {
+    @apply motion-reduce:w-transition-none w-transition w-duration-150 w-translate-y-1 w-opacity-0;
 
-  .tab-nav li {
-    width: auto;
-  }
-}
-
-// Media for Windows High Contrast
-@media (forced-colors: $media-forced-colours) {
-  .tab-nav li.active a {
-    border-bottom: 0.3em solid $system-color-link-text;
+    &.animate-in {
+      @apply w-translate-y-0 w-opacity-100;
+    }
   }
 }

+ 0 - 1
client/scss/core.scss

@@ -154,7 +154,6 @@ These are classes that provide overrides.
 @import 'overrides/utilities.dropdowns';
 @import 'overrides/utilities.focus';
 @import 'overrides/utilities.visuallyhidden';
-@import 'overrides/utilities.scrollbars';
 
 // Legacy utilities
 @import 'overrides/utilities.legacy';

+ 5 - 0
client/scss/elements/_elements.scss

@@ -45,3 +45,8 @@ img {
   border-width: 0;
   border-style: solid;
 }
+
+::before,
+::after {
+  --tw-content: '';
+}

+ 0 - 25
client/scss/overrides/_utilities.scrollbars.scss

@@ -1,25 +0,0 @@
-.u-scrollbar-thin {
-  // Scrollbar styling for firefox
-  // https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color
-  scrollbar-color: theme('colors.grey.100') theme('colors.white.DEFAULT');
-  scrollbar-width: thin;
-
-  //Custom scrollbar styling for Safari & Chrome Windows / Mac / Android.
-  &::-webkit-scrollbar {
-    width: 5px;
-    height: 5px;
-  }
-
-  &::-webkit-scrollbar-button {
-    @apply w-hidden;
-    // Hide the scrollbar arrows on windows
-  }
-
-  &::-webkit-scrollbar-thumb {
-    @apply w-bg-grey-200 w-rounded-sm;
-  }
-
-  &::-webkit-scrollbar-track {
-    @apply w-bg-white;
-  }
-}

+ 1 - 1
client/src/components/Sidebar/modules/MainMenu.tsx

@@ -202,7 +202,7 @@ export const Menu: React.FunctionComponent<MenuProps> = ({
   };
 
   const className =
-    'sidebar-main-menu u-scrollbar-thin' +
+    'sidebar-main-menu w-scrollbar-thin' +
     (accountSettingsOpen ? ' sidebar-main-menu--open-footer' : '');
 
   return (

+ 1 - 1
client/src/components/Sidebar/modules/__snapshots__/MainMenu.test.js.snap

@@ -4,7 +4,7 @@ exports[`Menu should render with the minimum required props 1`] = `
 <Fragment>
   <nav
     aria-label="Main menu"
-    className="sidebar-main-menu u-scrollbar-thin"
+    className="sidebar-main-menu w-scrollbar-thin"
   >
     <ul
       className="sidebar-main-menu__list"

+ 3 - 1
client/src/entrypoints/admin/comments.js

@@ -289,7 +289,9 @@ window.comments = (() => {
       .forEach(initAddCommentButton);
 
     // Attach the commenting app to the tab navigation, if it exists
-    const tabNavElement = formElement.querySelector('[data-tab-nav]');
+    const tabNavElement = formElement.querySelector(
+      '[data-tabs] [role="tablist"]',
+    );
     if (tabNavElement) {
       commentApp.setCurrentTab(tabNavElement.dataset.currentTab);
       tabNavElement.addEventListener('switch', (e) => {

+ 5 - 37
client/src/entrypoints/admin/core.js

@@ -11,6 +11,7 @@ function addMessage(status, text) {
     clearTimeout(addMsgTimeout);
   }, 100);
 }
+
 window.addMessage = addMessage;
 
 function escapeHtml(text) {
@@ -24,6 +25,7 @@ function escapeHtml(text) {
 
   return text.replace(/[&<>"']/g, (char) => map[char]);
 }
+
 window.escapeHtml = escapeHtml;
 
 function initTagField(id, autocompleteUrl, options) {
@@ -45,6 +47,7 @@ function initTagField(id, autocompleteUrl, options) {
 
   $('#' + id).tagit(finalOptions);
 }
+
 window.initTagField = initTagField;
 
 /*
@@ -218,6 +221,7 @@ function enableDirtyFormCheck(formSelector, options) {
     }
   });
 }
+
 window.enableDirtyFormCheck = enableDirtyFormCheck;
 
 $(() => {
@@ -240,7 +244,7 @@ $(() => {
   });
 
   /* Functions that need to run/rerun when active tabs are changed */
-  $(document).on('shown.bs.tab', () => {
+  document.addEventListener('tab-changed', () => {
     // Resize autosize textareas
     // eslint-disable-next-line func-names
     $('textarea[data-autosize-on]').each(function () {
@@ -249,42 +253,6 @@ $(() => {
     });
   });
 
-  /* tabs */
-  const showTab = (tabButtonElem) => {
-    $(tabButtonElem).tab('show');
-
-    // Update data-current-tab attribute on the [data-tab-nav] element
-    const tabNavElem = tabButtonElem.closest('[data-tab-nav]');
-    tabNavElem.dataset.currentTab = tabButtonElem.dataset.tab;
-
-    // Trigger switch event
-    tabNavElem.dispatchEvent(
-      new CustomEvent('switch', { detail: { tab: tabButtonElem.dataset.tab } }),
-    );
-  };
-
-  if (window.location.hash) {
-    /* look for a tab matching the URL hash and activate it if found */
-    const cleanedHash = window.location.hash.replace(/[^\w\-#]/g, '');
-    const tab = document.querySelector(
-      'a[href="' + cleanedHash + '"][data-tab]',
-    );
-    if (tab) showTab(tab);
-  }
-
-  // eslint-disable-next-line func-names
-  $(document).on('click', '[data-tab-nav] a', function (e) {
-    e.preventDefault();
-    showTab(this);
-    window.history.replaceState(null, null, $(this).attr('href'));
-  });
-
-  // eslint-disable-next-line func-names
-  $(document).on('click', '.tab-toggle', function (e) {
-    e.preventDefault();
-    $('[data-tab-nav] a[href="' + $(this).attr('href') + '"]').trigger('click');
-  });
-
   // eslint-disable-next-line func-names
   $('.dropdown').each(function () {
     const $dropdown = $(this);

+ 5 - 3
client/src/entrypoints/admin/page-editor.js

@@ -255,9 +255,11 @@ function initErrorDetection() {
   // now identify them on each tab
   // eslint-disable-next-line guard-for-in
   for (const index in errorSections) {
-    $('[data-tab-nav] a[href="#' + index + '"]')
-      .addClass('errors')
-      .attr('data-count', errorSections[index]);
+    $('[data-tabs] a[href="#' + index + '"]')
+      .find('.w-tabs__errors')
+      .addClass('w-tabs__errors--active')
+      .find('.w-tabs__errors-count')
+      .text(errorSections[index]);
   }
 }
 

+ 7 - 9
client/src/entrypoints/admin/task-chooser-modal.js

@@ -1,8 +1,9 @@
 import $ from 'jquery';
+import { initTabs } from '../../includes/tabs';
 
 const ajaxifyTaskCreateTab = (modal, jsonData) => {
   $(
-    '#new a.task-type-choice, #new a.choose-different-task-type',
+    '#tab-new a.task-type-choice, #tab-new a.choose-different-task-type',
     modal.body,
   ).on('click', function onClickNew() {
     modal.loadUrl(this.href);
@@ -28,7 +29,7 @@ const ajaxifyTaskCreateTab = (modal, jsonData) => {
           errorThrown +
           ' - ' +
           response.status;
-        $('#new', modal.body).append(
+        $('#tab-new', modal.body).append(
           '<div class="help-block help-critical">' +
             '<strong>' +
             jsonData.error_label +
@@ -59,12 +60,6 @@ const TASK_CHOOSER_MODAL_ONLOAD_HANDLERS = {
         fetchResults(this.href);
         return false;
       });
-
-      $('a.create-one-now').on('click', (e) => {
-        // Select upload form tab
-        $('a[href="#new"]').tab('show');
-        e.preventDefault();
-      });
     }
 
     const searchForm = $('form.task-search', modal.body);
@@ -118,13 +113,16 @@ const TASK_CHOOSER_MODAL_ONLOAD_HANDLERS = {
       const wait = setTimeout(search, 50);
       $(this).data('timer', wait);
     });
+
+    // Reinitialize tabs to hook up tab event listeners in the modal
+    initTabs();
   },
   task_chosen(modal, jsonData) {
     modal.respond('taskChosen', jsonData.result);
     modal.close();
   },
   reshow_create_tab(modal, jsonData) {
-    $('#new', modal.body).html(jsonData.htmlFragment);
+    $('#tab-new', modal.body).html(jsonData.htmlFragment);
     ajaxifyTaskCreateTab(modal, jsonData);
   },
 };

+ 2 - 0
client/src/entrypoints/admin/wagtailadmin.js

@@ -2,6 +2,7 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import { Icon, Portal, initUpgradeNotification, initSkipLink } from '../..';
 import { initModernDropdown, initTooltips } from '../../includes/initTooltips';
+import { initTabs } from '../../includes/tabs';
 
 if (process.env.NODE_ENV === 'development') {
   // Run react-axe in development only, so it does not affect performance
@@ -24,5 +25,6 @@ document.addEventListener('DOMContentLoaded', () => {
   initUpgradeNotification();
   initTooltips();
   initModernDropdown();
+  initTabs();
   initSkipLink();
 });

+ 10 - 5
wagtail/documents/static_src/wagtaildocs/js/document-chooser-modal.js → client/src/entrypoints/documents/document-chooser-modal.js

@@ -1,3 +1,6 @@
+import $ from 'jquery';
+import { initTabs } from '../../includes/tabs';
+
 function ajaxifyDocumentUploadForm(modal) {
   $('form.document-upload', modal.body).on('submit', function () {
     var formdata = new FormData(this);
@@ -17,7 +20,7 @@ function ajaxifyDocumentUploadForm(modal) {
           errorThrown +
           ' - ' +
           response.status;
-        $('#upload', modal.body).append(
+        $('#tab-upload', modal.body).append(
           '<div class="help-block help-critical">' +
             '<strong>' +
             jsonData.error_label +
@@ -67,7 +70,7 @@ function ajaxifyDocumentUploadForm(modal) {
   });
 }
 
-DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS = {
+window.DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS = {
   chooser: function (modal, jsonData) {
     function ajaxifyLinks(context) {
       $('a.document-choice', context).on('click', function () {
@@ -87,8 +90,6 @@ DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS = {
           $('#id_document-chooser-upload-collection').val(collectionId);
         }
 
-        // Select upload form tab
-        $('a[href="#upload"]').tab('show');
         e.preventDefault();
       });
     }
@@ -134,13 +135,17 @@ DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS = {
     });
 
     $('#collection_chooser_collection_id').on('change', search);
+
+    // Reinitialize tabs to hook up tab event listeners in the modal
+    initTabs();
   },
   document_chosen: function (modal, jsonData) {
     modal.respond('documentChosen', jsonData.result);
     modal.close();
   },
   reshow_upload_form: function (modal, jsonData) {
-    $('#upload', modal.body).html(jsonData.htmlFragment);
+    $('#tab-upload', modal.body).replaceWith(jsonData.htmlFragment);
+    initTabs();
     ajaxifyDocumentUploadForm(modal);
   },
 };

+ 10 - 3
wagtail/images/static_src/wagtailimages/js/image-chooser-modal.js → client/src/entrypoints/images/image-chooser-modal.js

@@ -1,3 +1,6 @@
+import $ from 'jquery';
+import { initTabs } from '../../includes/tabs';
+
 function ajaxifyImageUploadForm(modal) {
   $('form.image-upload', modal.body).on('submit', function () {
     var formdata = new FormData(this);
@@ -29,7 +32,7 @@ function ajaxifyImageUploadForm(modal) {
             errorThrown +
             ' - ' +
             response.status;
-          $('#upload').append(
+          $('#tab-upload').append(
             '<div class="help-block help-critical">' +
               '<strong>' +
               jsonData.error_label +
@@ -80,7 +83,7 @@ function ajaxifyImageUploadForm(modal) {
   });
 }
 
-IMAGE_CHOOSER_MODAL_ONLOAD_HANDLERS = {
+window.IMAGE_CHOOSER_MODAL_ONLOAD_HANDLERS = {
   chooser: function (modal, jsonData) {
     var searchForm = $('form.image-search', modal.body);
     var searchUrl = searchForm.attr('action');
@@ -143,13 +146,17 @@ IMAGE_CHOOSER_MODAL_ONLOAD_HANDLERS = {
       });
       return false;
     });
+
+    // Reinitialize tabs to hook up tab event listeners in the modal
+    initTabs();
   },
   image_chosen: function (modal, jsonData) {
     modal.respond('imageChosen', jsonData.result);
     modal.close();
   },
   reshow_upload_form: function (modal, jsonData) {
-    $('#upload', modal.body).replaceWith(jsonData.htmlFragment);
+    $('#tab-upload', modal.body).replaceWith(jsonData.htmlFragment);
+    initTabs();
     ajaxifyImageUploadForm(modal);
   },
   select_format: function (modal) {

+ 1 - 0
client/src/entrypoints/images/image-chooser.js

@@ -95,4 +95,5 @@ function createImageChooser(id) {
 
   return chooser;
 }
+
 window.createImageChooser = createImageChooser;

+ 3 - 0
client/src/includes/breadcrumbs.js

@@ -1,5 +1,8 @@
 export default function initCollapsibleBreadcrumbs() {
   const breadcrumbsContainer = document.querySelector('[data-breadcrumb-next]');
+  if (!breadcrumbsContainer) {
+    return;
+  }
   const breadcrumbsToggle = breadcrumbsContainer.querySelector(
     '[data-toggle-breadcrumbs]',
   );

+ 327 - 0
client/src/includes/tabs.js

@@ -0,0 +1,327 @@
+/**
+ *  All tabs and tab content must be nested in an element with the data-tab attribute
+ *  All tab buttons need the role="tab" attr and an href with the tab content ID
+ *  Tab contents need to have the role="tabpanel" attribute and and ID attribute that matches the href of the tab link.
+ *  Tab buttons should also be wrapped in an element with the role="tablist" attribute
+ */
+class Tabs {
+  constructor(node) {
+    this.tabContainer = node;
+    this.tabButtons = this.tabContainer.querySelectorAll('[role="tab"]');
+    this.tabList = this.tabContainer.querySelector('[role="tablist"]');
+    this.tabPanels = this.tabContainer.querySelectorAll('[role="tabpanel"]');
+    this.keydownEventListener = this.keydownEventListener.bind(this);
+
+    // Tab Options - Add these data attributes along side the data-tabs attribute
+    // Use this to enable fade-in animations on tab select
+    this.animate = this.tabContainer.hasAttribute('data-tabs-animate');
+    // Disable url hash from appearing on tab select (normally used in modals)
+    this.disableURL = this.tabContainer.hasAttribute('data-tabs-disable-url');
+
+    this.state = {
+      // Tab Settings
+      activeTabID: '',
+      transition: 150,
+      initialPageLoad: true,
+      // CSS Classes
+      css: {
+        animate: 'animate-in',
+      },
+      // Keyboard Keys
+      keys: {
+        end: 'End',
+        home: 'Home',
+        left: 'ArrowLeft',
+        up: 'ArrowUp',
+        right: 'ArrowRight',
+        down: 'ArrowDown',
+      },
+      direction: {
+        ArrowLeft: -1,
+        ArrowRight: 1,
+      },
+    };
+
+    this.onComponentLoaded();
+  }
+
+  onComponentLoaded() {
+    this.bindEvents();
+
+    // Set active tab from url or make first tab active
+    if (this.tabButtons) {
+      // Set each button's aria-controls attribute and select tab if aria-selected has already been set on the element
+      this.tabButtons.forEach((button) => {
+        button.setAttribute(
+          'aria-controls',
+          button.getAttribute('href').replace('#', ''),
+        );
+      });
+
+      // Check for active items set by the template
+      const tabActive = [...this.tabButtons].find(
+        (button) => button.getAttribute('aria-selected') === 'true',
+      );
+
+      if (window.location.hash && !this.disableURL) {
+        this.selectTabByURLHash();
+      } else if (tabActive) {
+        // If a tab isn't hidden for some reason hide it
+        this.tabPanels.forEach((tab) => {
+          // eslint-disable-next-line no-param-reassign
+          tab.hidden = true;
+        });
+        // Show aria-selected tab
+        this.selectTab(tabActive);
+      } else {
+        this.selectFirstTab();
+      }
+    }
+  }
+
+  /**
+   * @param {string}newTabId
+   */
+  unSelectActiveTab(newTabId) {
+    // IF new tab ID is the current then don't transition out
+    if (newTabId === this.state.activeTabID || !this.state.activeTabID) {
+      return;
+    }
+
+    // Tab Content to deactivate
+    const tabContent = this.tabContainer.querySelector(
+      `#${this.state.activeTabID}`,
+    );
+
+    if (!tabContent) {
+      return;
+    }
+
+    if (this.animate) {
+      this.animateOut(tabContent);
+    } else {
+      tabContent.hidden = true;
+    }
+
+    const tab = this.tabContainer.querySelector(
+      `a[href='#${this.state.activeTabID}']`,
+    );
+
+    tab.setAttribute('aria-selected', 'false');
+    tab.setAttribute('tabindex', '-1');
+  }
+
+  selectTab(tab) {
+    if (!tab) {
+      return;
+    }
+
+    const tabContentId = tab.getAttribute('aria-controls');
+
+    // Unselect currently active tab
+    if (tabContentId) {
+      this.unSelectActiveTab(tabContentId);
+    }
+
+    this.state.activeTabID = tabContentId;
+
+    const linkedTab = this.tabContainer.querySelector(
+      `a[href="${tab.getAttribute('href')}"][role="tab"]`,
+    );
+
+    // If an external button was used to trigger the tab, make sure active tab is marked active
+    if (linkedTab) {
+      linkedTab.setAttribute('aria-selected', 'true');
+      linkedTab.removeAttribute('tabindex');
+    }
+
+    tab.setAttribute('aria-selected', 'true');
+    tab.removeAttribute('tabindex');
+
+    const tabContent = this.tabContainer.querySelector(`#${tabContentId}`);
+    if (!tabContent) {
+      return;
+    }
+
+    if (this.animate) {
+      this.animateIn(tabContent);
+    } else {
+      tabContent.hidden = false;
+    }
+
+    if (this.state.initialPageLoad) {
+      // On first load set the scroll to top to avoid scrolling to active section and header covering up tabs
+      setTimeout(() => {
+        window.scrollTo(0, 0);
+      }, this.state.transition * 2);
+    }
+
+    // Dispatch tab selected event for the rest of the admin to hook into if needed
+    // Trigger tab specific switch event
+    this.tabList.dispatchEvent(
+      new CustomEvent('switch', { detail: { tab: tab.dataset.tab } }),
+    );
+    // Dispatch tab-changed event on the document
+    document.dispatchEvent(new CustomEvent('tab-changed'));
+
+    // Set URL hash and browser history
+    if (!this.disableURL) {
+      this.setURLHash(tabContentId);
+    }
+  }
+
+  /**
+   * Fade Up and In animation
+   * @param tabContent{HTMLElement}
+   */
+  animateIn(tabContent) {
+    setTimeout(() => {
+      // eslint-disable-next-line no-param-reassign
+      tabContent.hidden = false;
+      // Wait for hidden attribute to be applied then fade in
+      setTimeout(() => {
+        tabContent.classList.add(this.state.css.animate);
+      }, this.state.transition);
+    }, this.state.transition);
+  }
+
+  /**
+   * Fade Down and Out by removing css class
+   * @param tabContent{HTMLElement}
+   */
+  animateOut(tabContent) {
+    // Wait element to transition out and then hide with hidden
+    tabContent.classList.remove(this.state.css.animate);
+    setTimeout(() => {
+      // eslint-disable-next-line no-param-reassign
+      tabContent.hidden = true;
+    }, this.state.transition);
+  }
+
+  bindEvents() {
+    if (!this.tabButtons) {
+      return;
+    }
+
+    this.tabButtons.forEach((tab, index) => {
+      tab.addEventListener('click', (e) => {
+        e.preventDefault();
+        this.selectTab(tab);
+      });
+      tab.addEventListener('focusin', () => {
+        this.selectTab(tab);
+      });
+      tab.addEventListener('keydown', this.keydownEventListener);
+      // Set index of tab used in keyboard controls
+      // eslint-disable-next-line no-param-reassign
+      tab.index = index;
+    });
+
+    // Select previous or next tab using history
+    window.addEventListener('popstate', (e) => {
+      if (e.state && e.state.tabContent) {
+        const tab = this.tabContainer.querySelector(
+          `a[href="#${e.state.tabContent}"][role="tab"]`,
+        );
+        if (tab) {
+          this.selectTab(tab);
+        }
+      }
+    });
+  }
+
+  /**
+   *  Handle keydown on tabs
+   * @param {Event}event
+   */
+  keydownEventListener(event) {
+    const keyPressed = event.key;
+    const { keys } = this.state;
+
+    switch (keyPressed) {
+      case keys.left:
+      case keys.right:
+        this.switchTabOnArrowPress(event);
+        break;
+      case keys.end:
+        event.preventDefault();
+        this.focusLastTab();
+        break;
+      case keys.home:
+        event.preventDefault();
+        this.focusFirstTab();
+        break;
+      default:
+        break;
+    }
+  }
+
+  selectTabByURLHash() {
+    if (window.location.hash) {
+      const cleanedHash = window.location.hash.replace(/[^\w\-#]/g, '');
+      const tab = this.tabContainer.querySelector(
+        `a[href="${cleanedHash}"][role="tab"]`,
+      );
+      if (tab) {
+        this.selectTab(tab);
+      } else {
+        // The hash doesn't match a tab on the page then select first tab
+        this.selectFirstTab();
+      }
+    }
+  }
+
+  /**
+   * Set url to have tab an tab hash at the end
+   */
+  setURLHash(tabId) {
+    if (
+      !this.state.initialPageLoad &&
+      (!window.history.state || window.history.state.tabContent !== tabId)
+    ) {
+      // Add a new history item to the stack
+      window.history.pushState({ tabContent: tabId }, null, `#${tabId}`);
+    }
+    this.state.initialPageLoad = false;
+  }
+
+  // Either focus the next, previous, first, or last tab depending on key pressed
+  switchTabOnArrowPress(event) {
+    const pressed = event.key;
+    const { direction } = this.state;
+    const { keys } = this.state;
+    const tabs = this.tabButtons;
+
+    if (direction[pressed]) {
+      const target = event.target;
+      if (target.index !== undefined) {
+        if (tabs[target.index + direction[pressed]]) {
+          tabs[target.index + direction[pressed]].focus();
+        } else if (pressed === keys.left) {
+          this.focusLastTab();
+        } else if (pressed === keys.right) {
+          this.focusFirstTab();
+        }
+      }
+    }
+  }
+
+  focusFirstTab() {
+    this.tabButtons[0].focus();
+  }
+
+  focusLastTab() {
+    this.tabButtons[this.tabButtons.length - 1].focus();
+  }
+
+  selectFirstTab() {
+    this.selectTab(this.tabButtons[0]);
+    this.state.activeTabID = this.tabButtons[0].getAttribute('aria-controls');
+  }
+}
+
+export default Tabs;
+
+export const initTabs = (tabs = document.querySelectorAll('[data-tabs]')) => {
+  tabs.forEach((tabSet) => new Tabs(tabSet));
+};

+ 33 - 0
client/src/plugins/scrollbarThin.js

@@ -0,0 +1,33 @@
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const plugin = require('tailwindcss/plugin');
+
+module.exports = plugin(({ addComponents, theme }) => {
+  addComponents({
+    // Scrollbar styling for firefox
+    // https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color
+    '.scrollbar-thin': {
+      'scrollbarColor': `${theme('colors.grey.100')} ${theme(
+        'colors.white.DEFAULT',
+      )}`,
+      'scrollbarWidth': 'thin',
+
+      // Custom scrollbar styling for Safari & Chrome Windows / Mac / Android.
+      '&::-webkit-scrollbar': {
+        width: '5px',
+        height: '5px',
+      },
+      '&::-webkit-scrollbar-button': {
+        // Hide the scrollbar arrows on windows
+        display: 'none',
+      },
+      '&::-webkit-scrollbar-thumb': {
+        // Hide the scrollbar arrows on windows
+        backgroundColor: theme('colors.grey.200'),
+        borderRadius: theme('borderRadius.sm'),
+      },
+      '&::-webkit-scrollbar-track': {
+        background: theme('colors.transparent'),
+      },
+    },
+  });
+});

+ 2 - 1
client/tailwind.config.js

@@ -1,6 +1,5 @@
 const plugin = require('tailwindcss/plugin');
 const vanillaRTL = require('tailwindcss-vanilla-rtl');
-
 /**
  * Design Tokens
  */
@@ -24,6 +23,7 @@ const { spacing } = require('./src/tokens/spacing');
  * Plugins
  */
 const typeScale = require('./src/tokens/typeScale');
+const scrollbarThin = require('./src/plugins/scrollbarThin');
 
 /**
  * Functions
@@ -81,6 +81,7 @@ module.exports = {
   plugins: [
     typeScale,
     vanillaRTL,
+    scrollbarThin,
     /**
      * forced-colors media query for Windows High-Contrast mode support
      * See:

+ 10 - 2
client/webpack.config.js

@@ -56,8 +56,16 @@ module.exports = function exports(env, argv) {
       'workflow-status',
       'bulk-actions',
     ],
-    'images': ['image-chooser', 'image-chooser-telepath'],
-    'documents': ['document-chooser', 'document-chooser-telepath'],
+    'images': [
+      'image-chooser',
+      'image-chooser-modal',
+      'image-chooser-telepath',
+    ],
+    'documents': [
+      'document-chooser',
+      'document-chooser-modal',
+      'document-chooser-telepath',
+    ],
     'snippets': ['snippet-chooser', 'snippet-chooser-telepath'],
     'contrib/table_block': ['table'],
     'contrib/typed_table_block': ['typed_table_block'],

+ 3 - 2
docs/releases/3.0.md

@@ -19,6 +19,7 @@ Here are other changes related to the redesign:
  * Fully remove the legacy sidebar, with slim sidebar replacing it for all users (Thibaud Colas)
  * Add support for adding custom attributes for link menu items in the slim sidebar (Thibaud Colas)
  * Implement new slim page editor header with breadcrumb (Steven Steinwand, Karl Hobley)
+ * Implement new tabs design across the admin interface (Steven Steinwand)
 
 ### Removal of special-purpose field panel types
 
@@ -78,6 +79,7 @@ class LandingPage(Page):
  * Add the ability for choices to be separated by new lines instead of just commas within the form builder, commas will still be supported if used (Abdulmajeed Isa)
  * Add internationalisation UI to modeladmin (Andrés Martano)
  * Support chunking in `PageQuerySet.specific()` to reduce memory consumption (Andy Babic)
+ * Fix: Implement ARIA tabs markup and keyboards interactions for admin tabs (Steven Steinwand)
 
 ### Bug fixes
 
@@ -126,7 +128,7 @@ wagtail updatemodulepaths  # actually update the files
 ### Removed warning in Internet Explorer (IE11)
 
 * IE11 support was officially dropped in Wagtail 2.15, as of this release there will no longer be a warning shown to users of this browser.
-* Wagtail is fully compatible with Microsoft Edge, Microsoft’s replacement for Internet Explorer. You may consider using its `IE mode <https://docs.microsoft.com/en-us/deployedge/edge-ie-mode>`_ to keep access to IE11-only sites, while other sites and apps like Wagtail can leverage modern browser capabilities.
+* Wagtail is fully compatible with Microsoft Edge, Microsoft’s replacement for Internet Explorer. You may consider using its [IE mode](https://docs.microsoft.com/en-us/deployedge/edge-ie-mode) to keep access to IE11-only sites, while other sites and apps like Wagtail can leverage modern browser capabilities.
 
 ### Replaced `content_json` `TextField` with `content` `JSONField` in `PageRevision`
 
@@ -218,4 +220,3 @@ After setting the keyword argument, make sure to generate and run the migrations
 ### Removed support for Jinja2 2.x
 
 Jinja2 2.x is no longer supported as of this release; if you are using Jinja2 templating on your project, please upgrade to Jinja2 3.0 or above.
-

+ 0 - 138
wagtail/admin/static_src/wagtailadmin/js/vendor/bootstrap-tab.js

@@ -1,138 +0,0 @@
-/* ========================================================================
- * Bootstrap: tab.js v3.0.0
- * http://twbs.github.com/bootstrap/javascript.html#tabs
- * ========================================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ======================================================================== */
-
-
-+function ($) { "use strict";
-
-  // TAB CLASS DEFINITION
-  // ====================
-
-  var Tab = function (element) {
-    this.element = $(element)
-  }
-
-  Tab.prototype.show = function () {
-    var $this    = this.element
-    var $ul      = $this.closest('ul:not(.dropdown-menu)')
-    var selector = $this.attr('data-target')
-
-    if (!selector) {
-      selector = $this.attr('href')
-      selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
-    }
-
-    if ($this.parent('li').hasClass('active')) return
-
-    var previous = $ul.find('.active:last a')[0]
-    var e        = $.Event('show.bs.tab', {
-      relatedTarget: previous
-    })
-
-    $this.trigger(e)
-
-    if (e.isDefaultPrevented()) return
-
-    var $target = $(selector)
-
-    this.activate($this, $this.parent('li'), $ul)
-    this.activate($this, $target, $target.parent(), function () {
-      $this.trigger({
-        type: 'shown.bs.tab'
-      , relatedTarget: previous
-      })
-    })
-  }
-
-  Tab.prototype.activate = function (trigger, element, container, callback) {
-    var $active    = container.find('> .active')
-    var transition = callback
-      && $.support.transition
-      && $active.hasClass('fade')
-
-    function next() {
-      element.parent().find('.active').removeClass('active');
-      $active
-        .removeClass('active')
-        .find('> .dropdown-menu > .active')
-        .removeClass('active')
-     
-
-      trigger.addClass('active');
-      element.addClass('active');
-
-      if (transition) {
-        element[0].offsetWidth // reflow for transition
-        element.addClass('in')
-      } else {
-        element.removeClass('fade')
-      }
-
-      if (element.parent('.dropdown-menu')) {
-        element.closest('li.dropdown').addClass('active')
-      }
-
-      callback && callback()
-    }
-
-    transition ?
-      $active
-        .one($.support.transition.end, next)
-        .emulateTransitionEnd(150) :
-      next()
-
-    $active.removeClass('in')
-  }
-
-
-  // TAB PLUGIN DEFINITION
-  // =====================
-
-  var old = $.fn.tab
-
-  $.fn.tab = function ( option ) {
-    return this.each(function () {
-      var $this = $(this)
-      var data  = $this.data('bs.tab')
-
-      if (!data) $this.data('bs.tab', (data = new Tab(this)))
-      if (typeof option == 'string') data[option]()
-    })
-  }
-
-  $.fn.tab.Constructor = Tab
-
-
-  // TAB NO CONFLICT
-  // ===============
-
-  $.fn.tab.noConflict = function () {
-    $.fn.tab = old
-    return this
-  }
-
-
-  // TAB DATA-API
-  // ============
-
-  $(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) {
-    e.preventDefault()
-    $(this).tab('show')
-  })
-
-}(window.jQuery);

+ 68 - 50
wagtail/admin/templates/wagtailadmin/account/account.html

@@ -4,69 +4,87 @@
 {% block titletag %}{% trans "Account" %}{% endblock %}
 {% block content %}
     {% trans "Account" as account_str %}
-    {% include "wagtailadmin/shared/header.html" with title=account_str merged=1 tabbed=1 %}
+    {% include "wagtailadmin/shared/header.html" with title=account_str merged=1 %}
 
-    <ul class="tab-nav merged" data-tab-nav>
-        {% for tab in panels_by_tab.keys %}
-            <li{% if forloop.first %} class="active"{% endif %}><a href="#{{ tab.name }}">{{ tab.title }}</a></li>
-        {% endfor %}
+    <div class="w-tabs" data-tabs data-tabs-animate>
+        <div class="w-tabs__wrapper">
+            <div role="tablist" class="w-tabs__list nice-padding">
+                {% for tab in panels_by_tab.keys %}
+                    {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id=tab.name title=tab.title %}
+                {% endfor %}
 
-        {% if menu_items %}
-            <li><a href="#actions">{% trans "More actions" %}</a></li>
-        {% endif %}
-    </ul>
+                {% if menu_items %}
+                    {% trans 'More actions' as menu_items_title %}
+                    {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='actions' title=menu_items_title %}
+                {% endif %}
+            </div>
+        </div>
 
-    <form action="{% url 'wagtailadmin_account' %}" method="post" enctype="multipart/form-data" novalidate>
-        <div class="tab-content">
-            {% csrf_token %}
+        <form action="{% url 'wagtailadmin_account' %}" method="post" enctype="multipart/form-data" novalidate>
+            <div class="tab-content">
+                {% csrf_token %}
 
-            {% for tab, panels in panels_by_tab.items %}
-                <section id="{{ tab.name }}"{% if forloop.first %} class="active"{% endif %}>
-                    <ul class="objects">
-                        {% for panel in panels %}
-                            <li class="object">
-                                <div class="title-wrapper">
-                                    <label>{{ panel.title }}</label>
-                                </div>
-                                <div class="object-layout">
-                                    <div class="object-layout_big-part">
-                                        <div class="top-padding">
-                                            {{ panel.render }}
+                {% for tab, panels in panels_by_tab.items %}
+                    <section
+                        id="tab-{{ tab.name|cautious_slugify }}"
+                        class="w-tabs__panel"
+                        role="tabpanel"
+                        hidden
+                        aria-labelledby="tab-label-{{ tab.name|cautious_slugify }}"
+                    >
+                        <ul class="objects">
+                            {% for panel in panels %}
+                                <li class="object">
+                                    <div class="title-wrapper">
+                                        <label>{{ panel.title }}</label>
+                                    </div>
+                                    <div class="object-layout">
+                                        <div class="object-layout_big-part">
+                                            <div class="top-padding">
+                                                {{ panel.render }}
+                                            </div>
                                         </div>
                                     </div>
-                                </div>
-                            </li>
-                        {% endfor %}
-                    </ul>
+                                </li>
+                            {% endfor %}
+                        </ul>
 
-                    <div class="top-padding nice-padding">
-                        <button type="submit" class="button">{% trans 'Save account details' %}</button>
-                    </div>
-                </section>
-            {% endfor %}
+                        <div class="top-padding nice-padding">
+                            <button type="submit" class="button">{% trans 'Save account details' %}</button>
+                        </div>
+                    </section>
+                {% endfor %}
+
+                {% if menu_items %}
+                    <section
+                        id="tab-actions"
+                        class="w-tabs__panel"
+                        role="tabpanel"
+                        hidden
+                        aria-labelledby="tab-label-actions"
+                    >
+                        <ul class="listing">
+                            {% for item in menu_items %}
+                                <li class="row row-flush">
+                                    <div class="col6">
+                                        <a href="{{ item.url }}" class="button button-primary">{{ item.label }}</a>
+                                    </div>
+                                    <small class="col6">{{ item.help_text }}</small>
+                                </li>
+                            {% endfor %}
+                        </ul>
+                    </section>
+                {% endif %}
+            </div>
+        </form>
+    </div>
 
-            {% if menu_items %}
-                <section id="actions" class="nice-padding">
-                    <ul class="listing">
-                        {% for item in menu_items %}
-                            <li class="row row-flush">
-                                <div class="col6">
-                                    <a href="{{ item.url }}" class="button button-primary">{{ item.label }}</a>
-                                </div>
-                                <small class="col6">{{ item.help_text }}</small>
-                            </li>
-                        {% endfor %}
-                    </ul>
-                </section>
-            {% endif %}
-        </div>
-    </form>
 {% endblock %}
 
 {% block extra_css %}
     {{ block.super }}
     {% include "wagtailadmin/pages/_editor_css.html" %}
-    <link rel="stylesheet" href="{% versioned_static 'wagtailadmin/css/layouts/account.css' %}" type="text/css" />
+    <link rel="stylesheet" href="{% versioned_static 'wagtailadmin/css/layouts/account.css' %}" type="text/css"/>
     {{ media.css }}
 {% endblock %}
 {% block extra_js %}

+ 37 - 26
wagtail/admin/templates/wagtailadmin/panels/tabbed_interface.html

@@ -1,31 +1,42 @@
 {% load wagtailadmin_tags i18n %}
-<div class="tab-nav merged">
-    <ul data-tab-nav role="tablist" data-current-tab="{{ self.children.0.heading|cautious_slugify }}">
-        {% for child in self.children %}
-            <li class="{{ child.classes|join:" " }} {% if forloop.first %}active{% endif %}" role="tab" aria-controls="tab-{{ child.heading|cautious_slugify }}">
-                <a href="#tab-{{ child.heading|cautious_slugify }}" class="{% if forloop.first %}active{% endif %}" data-tab="{{ child.heading|cautious_slugify }}">{{ child.heading }}</a>
-            </li>
-        {% endfor %}
-    </ul>
-    {% if self.form.show_comments_toggle %}
-        <div class="right wide">
-            <div class="comments-controls" hidden data-comment-notifications>
-                <div class="comment-notifications-toggle">
-                    <label class="switch switch--teal-background">
-                        {% trans "Comment notifications" %}
-                        {{ self.form.comment_notifications }}
-                        <span class="switch__toggle"></span>
-                    </label>
+
+<div class="w-tabs" data-tabs data-tabs-animate>
+    <div class="w-tabs__wrapper">
+        <div role="tablist" class="w-tabs__list w-px-5 sm:w-px-[4.5rem]">
+            {% for child in self.children %}
+                {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id=child.heading title=child.heading classes=child.classes|join:" " %}
+            {% endfor %}
+        </div>
+    </div>
+
+    <template>
+        {# TODO To be re-implemented for comments side panel #}
+        {% if self.form.show_comments_toggle %}
+            <div class="right wide">
+                <div class="comments-controls" hidden data-comment-notifications>
+                    <div class="comment-notifications-toggle">
+                        <label class="switch switch--teal-background">
+                            {% trans "Comment notifications" %}
+                            {{ self.form.comment_notifications }}
+                            <span class="switch__toggle"></span>
+                        </label>
+                    </div>
                 </div>
             </div>
-        </div>
-    {% endif %}
-</div>
+        {% endif %}
+    </template>
 
-<div class="tab-content">
-    {% for child in self.children %}
-        <section id="tab-{{ child.heading|cautious_slugify }}" class="{{ child.classes|join:" " }} {% if forloop.first %}active{% endif %}" role="tabpanel" aria-labelledby="tab-label-{{ child.heading|cautious_slugify }}" data-tab="{{ child.heading|cautious_slugify }}">
-            {{ child.render_as_object }}
-        </section>
-    {% endfor %}
+    <div class="tab-content">
+        {% for child in self.children %}
+            <section
+                id="tab-{{ child.heading|cautious_slugify }}"
+                class="w-tabs__panel {{ child.classes|join:" " }}"
+                role="tabpanel"
+                aria-labelledby="tab-label-{{ child.heading|cautious_slugify }}"
+                hidden
+            >
+                {{ child.render_as_object }}
+            </section>
+        {% endfor %}
+    </div>
 </div>

+ 1 - 1
wagtail/admin/templates/wagtailadmin/shared/breadcrumb-next.html

@@ -8,7 +8,7 @@
 {% with breadcrumb_link_classes='w-text-grey-600 w-text-14 w-no-underline w-outline-offset-inside hover:w-underline hover:w-text-primary' breadcrumb_item_classes='w-flex w-items-center w-overflow-hidden w-transition w-duration-300 w-whitespace-nowrap w-flex-shrink-0' icon_classes='w-w-4 w-h-4 w-mr-3' %}
     {# Breadcrumbs are visible on mobile by default but hidden on desktop #}
 
-    <div class="w-flex w-flex-row w-items-center w-overflow-x-auto w-overflow-y-hidden u-scrollbar-thin" data-breadcrumb-next>
+    <div class="w-flex w-flex-row w-items-center w-overflow-x-auto w-overflow-y-hidden w-scrollbar-thin" data-breadcrumb-next>
         <button
             type="button"
             data-toggle-breadcrumbs

+ 2 - 3
wagtail/admin/templates/wagtailadmin/shared/header.html

@@ -8,16 +8,15 @@
     - `search_url` - if present, display a search box. This is a URL route name (taking no parameters) to be used as the action for that search box
     - `query_parameters` - a query string (without the '?') to be placed after the search URL
     - `icon` - name of an icon to place against the title
-    - `tabbed` - if true, add the classname 'tab-merged'
     - `merged` - if true, add the classname 'merged'
     - `action_url` - if present, display an 'action' button. This is the URL to be used as the link URL for the button
     - `action_text` - text for the 'action' button
     - `action_icon` - icon for the 'action' button, default is 'icon-plus'
 
 {% endcomment %}
-<header class="{% if merged %}merged{% endif %} {% if tabbed %}tab-merged{% endif %} {% if search_form %}hasform{% endif %}">
+<header class="{% if merged %}merged{% endif %} {% if search_form %}hasform{% endif %}">
     {% block breadcrumb %}{% endblock %}
-    <div class="row{% if not tabbed %} nice-padding{% endif %}">
+    <div class="row nice-padding">
         <div class="left">
             <div class="col header-title">
                 <h1>{% if icon %}{% icon name=icon class_name="header-title-icon" %}{% endif %}

+ 19 - 0
wagtail/admin/templates/wagtailadmin/shared/tabs/tab_nav_link.html

@@ -0,0 +1,19 @@
+{% load wagtailadmin_tags i18n %}
+
+{% comment %}
+    Variables accepted by this template:
+
+    - `tab_id` - {string} A unique tab id
+    - `title` - {string} Text that the tab button will display
+    - `active` - {boolean?} Force this to be active
+    - `classes` - {string?} Extra css classes to pass to this component
+    - `errors_count` - {number?} Show above the tab for errors count
+{% endcomment %}
+
+<a id="tab-label-{{ tab_id|cautious_slugify }}" href="#tab-{{ tab_id|cautious_slugify }}" class="w-tabs__tab {{ classes }}" role="tab" aria-selected="false" tabindex="-1">
+    <div class="w-tabs__errors {% if errors_count %}w-tabs__errors--active{% endif %}">
+        <span class="w-sr-only">{% trans 'Errors Count: ' %}</span>
+        <span class="w-tabs__errors-count">{{ errors_count }}</span>
+    </div>
+    {{ title }}
+</a>

+ 45 - 15
wagtail/admin/templates/wagtailadmin/workflows/task_chooser/chooser.html

@@ -1,21 +1,51 @@
 {% load i18n %}
 {% trans "Choose a task" as  choose_str %}
-{% include "wagtailadmin/shared/header.html" with title=choose_str tabbed=1 merged=1 icon="thumbtack" %}
+{% include "wagtailadmin/shared/header.html" with title=choose_str merged=1 icon="thumbtack" %}
 
 {% if can_create %}
-    <ul class="tab-nav merged" data-tab-nav>
-        <li class="active"><a href="#new">{% trans "New" %}</a></li>
-        <li><a href="#existing">{% trans "Existing" %}</a></li>
-    </ul>
-{% endif %}
+    <div class="w-tabs" data-tabs data-tabs-disable-url>
+        <div class="w-tabs__wrapper w-overflow-hidden">
+            {# Using nice-padding and full width class until the modal header is restyled #}
+            <div role="tablist" class="w-tabs__list w-w-full nice-padding">
+                {% trans "New" as new_text %}
+                {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='new' title=new_text %}
+                {% trans "Existing" as existing_text %}
+                {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='existing' title=existing_text %}
+            </div>
+        </div>
 
-<div class="tab-content">
-    {% if can_create %}
-        <section id="new" class="active nice-padding">
-            {% include "wagtailadmin/workflows/task_chooser/includes/create_tab.html" %}
-        </section>
-    {% endif %}
-    <section id="existing" class="nice-padding{% if not can_create %} active{% endif %}">
+        <div class="tab-content nice-padding">
+            <section
+                id="tab-new"
+                class="w-tabs__panel"
+                role="tabpanel"
+                aria-labelledby="tab-label-new"
+                hidden
+            >
+                {% include "wagtailadmin/workflows/task_chooser/includes/create_tab.html" %}
+            </section>
+            <section
+                id="tab-existing"
+                class="w-tabs__panel"
+                role="tabpanel"
+                aria-labelledby="tab-label-existing"
+                hidden
+            >
+                <form class="task-search search-bar" action="{% url 'wagtailadmin_workflows:task_chooser_results' %}" method="GET" novalidate>
+                    <ul class="fields">
+                        {% for field in search_form %}
+                            {% include "wagtailadmin/shared/field_as_li.html" with field=field %}
+                        {% endfor %}
+                    </ul>
+                </form>
+                <div id="search-results" class="listing tasks">
+                    {% include "wagtailadmin/workflows/task_chooser/includes/results.html" %}
+                </div>
+            </section>
+        </div>
+    </div>
+{% else %}
+    <div class="nice-padding">
         <form class="task-search search-bar" action="{% url 'wagtailadmin_workflows:task_chooser_results' %}" method="GET" novalidate>
             <ul class="fields">
                 {% for field in search_form %}
@@ -26,5 +56,5 @@
         <div id="search-results" class="listing tasks">
             {% include "wagtailadmin/workflows/task_chooser/includes/results.html" %}
         </div>
-    </section>
-</div>
+    </div>
+{% endif %}

+ 1 - 1
wagtail/admin/templates/wagtailadmin/workflows/task_chooser/includes/results.html

@@ -67,7 +67,7 @@
             {% trans "You haven't created any tasks." %}
             {% if can_create %}
                 {% blocktrans trimmed %}
-                    Why not <a class="create-one-now" href="#">create one now</a>?
+                    Why not <a class="create-one-now" href="#tab-new" role="tab">create one now</a>?
                 {% endblocktrans %}
             {% endif %}
         </p>

+ 9 - 9
wagtail/admin/tests/pages/test_create_page.py

@@ -145,10 +145,11 @@ class TestPageCreation(TestCase, WagtailTestUtils):
         self.assertEqual(response["Content-Type"], "text/html; charset=utf-8")
         self.assertContains(
             response,
-            '<a href="#tab-content" class="active" data-tab="content">Content</a>',
+            '<a id="tab-label-content" href="#tab-content" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
         )
         self.assertContains(
-            response, '<a href="#tab-promote" class="" data-tab="promote">Promote</a>'
+            response,
+            '<a id="tab-label-promote" href="#tab-promote" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
         )
         # test register_page_action_menu_item hook
         self.assertContains(
@@ -215,11 +216,9 @@ class TestPageCreation(TestCase, WagtailTestUtils):
         self.assertEqual(response.status_code, 200)
         self.assertContains(
             response,
-            '<a href="#tab-content" class="active" data-tab="content">Content</a>',
-        )
-        self.assertNotContains(
-            response, '<a href="#tab-promote" class="" data-tab="promote">Promote</a>'
+            '<a id="tab-label-content" href="#tab-content" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
         )
+        self.assertNotContains(response, "tab-promote")
 
     def test_create_page_with_custom_tabs(self):
         """
@@ -234,14 +233,15 @@ class TestPageCreation(TestCase, WagtailTestUtils):
         self.assertEqual(response.status_code, 200)
         self.assertContains(
             response,
-            '<a href="#tab-content" class="active" data-tab="content">Content</a>',
+            '<a id="tab-label-content" href="#tab-content" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
         )
         self.assertContains(
-            response, '<a href="#tab-promote" class="" data-tab="promote">Promote</a>'
+            response,
+            '<a id="tab-label-promote" href="#tab-promote" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
         )
         self.assertContains(
             response,
-            '<a href="#tab-dinosaurs" class="" data-tab="dinosaurs">Dinosaurs</a>',
+            '<a id="tab-label-dinosaurs" href="#tab-dinosaurs" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
         )
 
     def test_create_page_with_non_model_field(self):

+ 5 - 11
wagtail/admin/tests/test_edit_handlers.py

@@ -419,23 +419,17 @@ class TestTabbedInterface(TestCase):
 
         # result should contain tab buttons
         self.assertIn(
-            '<a href="#tab-event-details" class="active" data-tab="event-details">Event details</a>',
+            '<a id="tab-label-event-details" href="#tab-event-details" class="w-tabs__tab shiny" role="tab" aria-selected="false" tabindex="-1">',
             result,
         )
         self.assertIn(
-            '<a href="#tab-speakers" class="" data-tab="speakers">Speakers</a>', result
+            '<a id="tab-label-speakers" href="#tab-speakers" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
+            result,
         )
 
         # result should contain tab panels
-        self.assertIn('<div class="tab-content">', result)
-        self.assertIn(
-            '<section id="tab-event-details" class="shiny active" role="tabpanel" aria-labelledby="tab-label-event-details" data-tab="event-details">',
-            result,
-        )
-        self.assertIn(
-            '<section id="tab-speakers" class=" " role="tabpanel" aria-labelledby="tab-label-speakers" data-tab="speakers">',
-            result,
-        )
+        self.assertIn('aria-labelledby="tab-label-event-details"', result)
+        self.assertIn('aria-labelledby="tab-label-speakers"', result)
 
         # result should contain rendered content from descendants
         self.assertIn("Abergavenny sheepdog trials</textarea>", result)

+ 1 - 1
wagtail/contrib/modeladmin/templates/modeladmin/choose_parent.html

@@ -21,7 +21,7 @@
 {% block content %}
 
     {% block header %}
-        {% include "modeladmin/includes/header_with_breadcrumb.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=True %}
+        {% include "modeladmin/includes/header_with_breadcrumb.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon %}
     {% endblock %}
 
     <div>

+ 1 - 1
wagtail/contrib/modeladmin/templates/modeladmin/create.html

@@ -35,7 +35,7 @@
 {% block content %}
 
     {% block header %}
-        {% include "wagtailadmin/shared/header_with_locale_selector.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=1 merged=1 %}
+        {% include "wagtailadmin/shared/header_with_locale_selector.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon merged=1 %}
     {% endblock %}
 
     <form action="{% block form_action %}{{ view.create_url }}{% endblock %}{% if locale %}?locale={{ locale.language_code }}{% endif %}"{% if is_multipart %} enctype="multipart/form-data"{% endif %} method="POST" novalidate>

+ 1 - 1
wagtail/contrib/modeladmin/templates/modeladmin/edit.html

@@ -2,7 +2,7 @@
 {% load i18n wagtailadmin_tags %}
 
 {% block header %}
-    {% include "modeladmin/includes/header_with_history.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=1 merged=1 latest_log_entry=latest_log_entry history_url=history_url %}
+    {% include "modeladmin/includes/header_with_history.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon merged=1 latest_log_entry=latest_log_entry history_url=history_url %}
 {% endblock %}
 
 {% block form_action %}{{ view.edit_url }}{% endblock %}

+ 1 - 1
wagtail/contrib/modeladmin/templates/modeladmin/inspect.html

@@ -17,7 +17,7 @@
 {% block content %}
 
     {% block header %}
-        {% include "modeladmin/includes/header_with_breadcrumb.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=True %}
+        {% include "modeladmin/includes/header_with_breadcrumb.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon %}
     {% endblock %}
 
     <div>

+ 1 - 1
wagtail/contrib/settings/templates/wagtailsettings/edit.html

@@ -3,7 +3,7 @@
 {% block titletag %}{% blocktrans trimmed %}Editing {{ setting_type_name}} - {{ instance }}{% endblocktrans %}{% endblock %}
 {% block bodyclass %}menu-settings{% endblock %}
 {% block content %}
-    <header class="nice-padding {% if tabbed %}merged tab-merged{% endif %}">
+    <header class="nice-padding merged">
         <div class="row">
             <div class="left">
                 <div class="col">

+ 12 - 17
wagtail/contrib/styleguide/templates/wagtailstyleguide/base.html

@@ -685,26 +685,21 @@
         <section id="tabs">
             <h2>Tabs</h2>
 
-            <ul class="tab-nav" data-tab-nav>
-                <li class="active"><a href="#tab1">Tab 1</a></li>
-                <li><a href="#tab2">Tab 2</a></li>
-            </ul>
-
-            <p>Tabs are currently only used following headers, where they often appear merged with the bottom of the header:</p>
-
-            {% include "wagtailadmin/shared/header.html" with title=title_trans merged=1 %}
-            <ul class="tab-nav merged" data-tab-nav>
-                <li class="active"><a href="#">Tab1</a></li>
-                <li><a href="#">Tab2</a></li>
-            </ul>
+            <div class="w-tabs" data-tabs data-tabs-animate>
+                <div role="tablist" class="w-tabs__list">
+                    {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='tab-1' title='Tab 1' %}
+                    {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='tab-2' title='Tab 2' %}
+                </div>
+            </div>
 
             <p>Tabs can also indicate errors:</p>
 
-            {% include "wagtailadmin/shared/header.html" with title=title_trans merged=1 %}
-            <ul class="tab-nav merged" data-tab-nav>
-                <li class="active"><a href="#" class="errors" data-count="123">Tab1</a></li>
-                <li><a href="#" class="errors" data-count="1">Tab2</a></li>
-            </ul>
+            <div class="w-tabs" data-tabs data-tabs-animate>
+                <div role="tablist" class="w-tabs__list">
+                    {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='tab-errors-1' title='Tab 1' errors_count='5' %}
+                    {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='tab-errors-2' title='Tab 2' errors_count='55' %}
+                </div>
+            </div>
         </section>
 
         <section id="breadcrumbs">

+ 37 - 25
wagtail/documents/templates/wagtaildocs/chooser/chooser.html

@@ -1,34 +1,46 @@
 {% load i18n wagtailadmin_tags %}
 {% trans "Choose a document" as  choose_str %}
-{% include "wagtailadmin/shared/header.html" with title=choose_str tabbed=1 merged=1 icon="doc-full-inverse" %}
+{% include "wagtailadmin/shared/header.html" with title=choose_str merged=1 icon="doc-full-inverse" %}
 
 {{ uploadform.media.js }}
 {{ uploadform.media.css }}
 
-{% if uploadform %}
-    <ul class="tab-nav merged" data-tab-nav>
-        <li class="{% if not uploadform.errors %}active {% endif %}"><a href="#search">{% trans "Search" %}</a></li>
-        <li class="{% if uploadform.errors %}active {% endif %}"><a href="#upload">{% trans "Upload" %}</a></li>
-    </ul>
-{% endif %}
-
-<div class="tab-content">
-    <section id="search" class="{% if not uploadform.errors %}active {% endif %}nice-padding">
-        <form class="document-search search-bar" action="{% url 'wagtaildocs:chooser_results' %}" method="GET" novalidate>
-            <ul class="fields">
-                {% for field in searchform %}
-                    {% include "wagtailadmin/shared/field_as_li.html" with field=field %}
-                {% endfor %}
-                {% if collections %}
-                    {% include "wagtailadmin/shared/collection_chooser.html" %}
-                {% endif %}
-            </ul>
-        </form>
-        <div id="search-results" class="listing documents">
-            {% include "wagtaildocs/chooser/results.html" %}
-        </div>
-    </section>
+<div class="w-tabs" data-tabs data-tabs-disable-url>
     {% if uploadform %}
-        {% include "wagtaildocs/chooser/upload_form.html" with form=uploadform %}
+        <div class="w-tabs__wrapper w-overflow-hidden">
+            {# Using nice-padding and full width class until the modal header is restyled #}
+            <div role="tablist" class="w-tabs__list w-w-full nice-padding">
+                {% trans "Search" as search_text %}
+                {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='search' title=search_text %}
+                {% trans "Upload" as upload_text %}
+                {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='upload' title=upload_text %}
+            </div>
+        </div>
     {% endif %}
+
+    <div class="tab-content nice-padding">
+        <section
+            id="tab-search"
+            class="w-tabs__panel"
+            role="tabpanel"
+            aria-labelledby="tab-label-search"
+        >
+            <form class="document-search search-bar" action="{% url 'wagtaildocs:chooser_results' %}" method="GET" novalidate>
+                <ul class="fields">
+                    {% for field in searchform %}
+                        {% include "wagtailadmin/shared/field_as_li.html" with field=field %}
+                    {% endfor %}
+                    {% if collections %}
+                        {% include "wagtailadmin/shared/collection_chooser.html" %}
+                    {% endif %}
+                </ul>
+            </form>
+            <div id="search-results" class="listing documents">
+                {% include "wagtaildocs/chooser/results.html" %}
+            </div>
+        </section>
+        {% if uploadform %}
+            {% include "wagtaildocs/chooser/upload_form.html" with form=uploadform %}
+        {% endif %}
+    </div>
 </div>

+ 1 - 2
wagtail/documents/templates/wagtaildocs/chooser/results.html

@@ -25,9 +25,8 @@
                 {% trans "You haven't uploaded any documents." %}
             {% endif %}
             {% if uploadform %}
-                {% url 'wagtaildocs:add_multiple' as wagtaildocs_add_document_url %}
                 {% blocktrans trimmed %}
-                    Why not <a class="upload-one-now" href="{{ wagtaildocs_add_document_url }}">upload one now</a>?
+                    Why not <a class="upload-one-now" href="#tab-upload" role="tab">upload one now</a>?
                 {% endblocktrans %}
             {% endif %}
         </p>

+ 7 - 1
wagtail/documents/templates/wagtaildocs/chooser/upload_form.html

@@ -1,5 +1,11 @@
 {% load i18n wagtailadmin_tags %}
-<section id="upload" class="{% if form.errors %}active {% endif %}nice-padding">
+<section
+    id="tab-upload"
+    class="w-tabs__panel"
+    role="tabpanel"
+    hidden
+    aria-labelledby="tab-label-upload"
+>
     {% include "wagtailadmin/shared/non_field_errors.html" with form=form %}
     <form class="document-upload" action="{% url 'wagtaildocs:chooser_upload' %}" method="POST" enctype="multipart/form-data" novalidate>
         {% csrf_token %}

+ 43 - 33
wagtail/images/templates/wagtailimages/chooser/chooser.html

@@ -1,43 +1,53 @@
 {% load wagtailimages_tags wagtailadmin_tags %}
 {% load i18n %}
 {% trans "Choose an image" as choose_str %}
-{% include "wagtailadmin/shared/header.html" with title=choose_str merged=1 tabbed=1 icon="image" %}
+{% include "wagtailadmin/shared/header.html" with title=choose_str merged=1 icon="image" %}
 
 {{ uploadform.media.js }}
 {{ uploadform.media.css }}
 
-{% if uploadform %}
-    <ul class="tab-nav merged" data-tab-nav>
-        <li class="{% if not uploadform.errors %}active{% endif %}"><a href="#search" >{% trans "Search" %}</a></li>
-        <li class="{% if uploadform.errors %}active{% endif %}"><a href="#upload">{% trans "Upload" %}</a></li>
-    </ul>
-{% endif %}
-
-<div class="tab-content">
-    <section id="search" class="{% if not uploadform.errors %}active{% endif %} nice-padding">
-        <form class="image-search search-bar" action="{% url 'wagtailimages:chooser_results' %}{% if will_select_format %}?select_format=true{% endif %}" method="GET" autocomplete="off" novalidate>
-            <ul class="fields">
-                {% for field in searchform %}
-                    {% include "wagtailadmin/shared/field_as_li.html" with field=field %}
-                {% endfor %}
-                {% if collections %}
-                    {% include "wagtailadmin/shared/collection_chooser.html" %}
-                {% endif %}
-                {% if popular_tags %}
-                    <li class="taglist">
-                        <h3>{% trans 'Popular tags' %}</h3>
-                        {% for tag in popular_tags %}
-                            <a class="suggested-tag tag" href="{% url 'wagtailimages:index' %}?tag={{ tag.name|urlencode }}">{{ tag.name }}</a>
-                        {% endfor %}
-                    </li>
-                {% endif %}
-            </ul>
-        </form>
-        <div id="image-results">
-            {% include "wagtailimages/chooser/results.html" %}
-        </div>
-    </section>
+<div class="w-tabs" data-tabs data-tabs-disable-url>
     {% if uploadform %}
-        {% include "wagtailimages/chooser/upload_form.html" with form=uploadform will_select_format=will_select_format %}
+        {# Using nice-padding and full width class until the modal header is restyled #}
+        <div role="tablist" class="w-tabs__list w-w-full nice-padding">
+            {% trans "Search" as search_text %}
+            {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='search' title=search_text active=uploadform.errors %}
+            {% trans "Upload" as upload_text %}
+            {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='upload' title=upload_text active=uploadform.errors %}
+        </div>
     {% endif %}
+
+    <div class="tab-content nice-padding">
+        <section
+            id="tab-search"
+            class="w-tabs__panel"
+            role="tabpanel"
+            aria-labelledby="tab-label-search"
+        >
+            <form class="image-search search-bar" action="{% url 'wagtailimages:chooser_results' %}{% if will_select_format %}?select_format=true{% endif %}" method="GET" autocomplete="off" novalidate>
+                <ul class="fields">
+                    {% for field in searchform %}
+                        {% include "wagtailadmin/shared/field_as_li.html" with field=field %}
+                    {% endfor %}
+                    {% if collections %}
+                        {% include "wagtailadmin/shared/collection_chooser.html" %}
+                    {% endif %}
+                    {% if popular_tags %}
+                        <li class="taglist">
+                            <h3>{% trans 'Popular tags' %}</h3>
+                            {% for tag in popular_tags %}
+                                <a class="suggested-tag tag" href="{% url 'wagtailimages:index' %}?tag={{ tag.name|urlencode }}">{{ tag.name }}</a>
+                            {% endfor %}
+                        </li>
+                    {% endif %}
+                </ul>
+            </form>
+            <div id="image-results">
+                {% include "wagtailimages/chooser/results.html" %}
+            </div>
+        </section>
+        {% if uploadform %}
+            {% include "wagtailimages/chooser/upload_form.html" with form=uploadform will_select_format=will_select_format %}
+        {% endif %}
+    </div>
 </div>

+ 7 - 2
wagtail/images/templates/wagtailimages/chooser/upload_form.html

@@ -1,6 +1,11 @@
 {% load i18n wagtailadmin_tags %}
-
-<section id="upload" class="{% if form.errors %}active{% endif %} nice-padding">
+<section
+    id="tab-upload"
+    class="w-tabs__panel"
+    role="tabpanel"
+    hidden
+    aria-labelledby="tab-label-upload"
+>
     {% include "wagtailadmin/shared/non_field_errors.html" with form=form %}
     <form class="image-upload" action="{% url 'wagtailimages:chooser_upload' %}{% if will_select_format %}?select_format=true{% endif %}" method="POST" enctype="multipart/form-data" novalidate>
         {% csrf_token %}

+ 1 - 1
wagtail/snippets/templates/wagtailsnippets/snippets/create.html

@@ -3,7 +3,7 @@
 {% block titletag %}{% blocktrans trimmed with snippet_type_name=model_opts.verbose_name %}New  {{ snippet_type_name }}{% endblocktrans %}{% endblock %}
 {% block content %}
     {% trans "New" as new_str %}
-    {% include "wagtailadmin/shared/header_with_locale_selector.html" with title=new_str subtitle=model_opts.verbose_name icon="snippet" tabbed=1 merged=1 locale=locale translations=translations only %}
+    {% include "wagtailadmin/shared/header_with_locale_selector.html" with title=new_str subtitle=model_opts.verbose_name icon="snippet" merged=1 locale=locale translations=translations only %}
 
     <form action="{{ action_url }}" method="POST" novalidate{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
         {% csrf_token %}

+ 1 - 1
wagtail/snippets/templates/wagtailsnippets/snippets/edit.html

@@ -3,7 +3,7 @@
 {% block titletag %}{% blocktrans trimmed with snippet_type_name=model_opts.verbose_name %}Editing {{ snippet_type_name }} - {{ instance }}{% endblocktrans %}{% endblock %}
 {% block content %}
     {% trans "Editing" as editing_str %}
-    {% include "wagtailsnippets/snippets/_header_with_history.html" with title=editing_str subtitle=instance icon="snippet" tabbed=1 merged=1 %}
+    {% include "wagtailsnippets/snippets/_header_with_history.html" with title=editing_str subtitle=instance icon="snippet" merged=1 %}
 
     <div class="row row-flush">
 

+ 8 - 38
wagtail/snippets/tests.py

@@ -372,19 +372,7 @@ class TestSnippetCreateView(TestCase, WagtailTestUtils):
         response = self.get()
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "wagtailsnippets/snippets/create.html")
-        self.assertNotContains(
-            response, '<ul data-tab-nav role="tablist" data-current-tab="advert">'
-        )
-        self.assertNotContains(
-            response,
-            '<a href="#tab-advert" class="active" data-tab="advert">Advert</a>',
-            html=True,
-        )
-        self.assertNotContains(
-            response,
-            '<a href="#tab-other" class="" data-tab="other">Other</a>',
-            html=True,
-        )
+        self.assertNotContains(response, 'role="tablist"', html=True)
 
     def test_snippet_with_tabbed_interface(self):
         response = self.client.get(
@@ -393,18 +381,14 @@ class TestSnippetCreateView(TestCase, WagtailTestUtils):
 
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "wagtailsnippets/snippets/create.html")
-        self.assertContains(
-            response, '<ul data-tab-nav role="tablist" data-current-tab="advert">'
-        )
+        self.assertContains(response, 'role="tablist"')
         self.assertContains(
             response,
-            '<a href="#tab-advert" class="active" data-tab="advert">Advert</a>',
-            html=True,
+            '<a id="tab-label-advert" href="#tab-advert" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
         )
         self.assertContains(
             response,
-            '<a href="#tab-other" class="" data-tab="other">Other</a>',
-            html=True,
+            '<a id="tab-label-other" href="#tab-other" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
         )
 
     def test_create_with_limited_permissions(self):
@@ -698,17 +682,7 @@ class TestSnippetEditView(BaseTestSnippetEditView):
         response = self.get()
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html")
-        self.assertNotContains(
-            response, '<ul data-tab-nav role="tablist" data-current-tab="advert">'
-        )
-        self.assertNotContains(
-            response,
-            '<a href="#advert" class="active" data-tab="advert">Advert</a>',
-            html=True,
-        )
-        self.assertNotContains(
-            response, '<a href="#other" class="" data-tab="other">Other</a>', html=True
-        )
+        self.assertNotContains(response, 'role="tablist"')
 
         # "Last updated" timestamp should be present
         self.assertContains(
@@ -914,18 +888,14 @@ class TestEditTabbedSnippet(BaseTestSnippetEditView):
 
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html")
-        self.assertContains(
-            response, '<ul data-tab-nav role="tablist" data-current-tab="advert">'
-        )
+        self.assertContains(response, 'role="tablist"')
         self.assertContains(
             response,
-            '<a href="#tab-advert" class="active" data-tab="advert">Advert</a>',
-            html=True,
+            '<a id="tab-label-advert" href="#tab-advert" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
         )
         self.assertContains(
             response,
-            '<a href="#tab-other" class="" data-tab="other">Other</a>',
-            html=True,
+            '<a id="tab-label-other" href="#tab-other" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
         )
 
 

+ 55 - 37
wagtail/users/templates/wagtailusers/users/create.html

@@ -5,46 +5,64 @@
 {% block content %}
 
     {% trans "Add user" as add_user_str %}
-    {% include "wagtailadmin/shared/header.html" with title=add_user_str merged=1 tabbed=1 icon="user" %}
+    {% include "wagtailadmin/shared/header.html" with title=add_user_str merged=1 icon="user" %}
 
-    <ul class="tab-nav merged" data-tab-nav>
-        <li class="active"><a href="#account">{% trans "Account" %}</a></li>
-        <li><a href="#roles">{% trans "Roles" %}</a></li>
-    </ul>
+    <div class="w-tabs" data-tabs data-tabs-animate>
+        <div class="w-tabs__wrapper">
+            <div role="tablist" class="w-tabs__list nice-padding">
+                {% trans "Account" as account_text %}
+                {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='account' title=account_text %}
+                {% trans "Roles" as roles_text %}
+                {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='roles' title=roles_text %}
+            </div>
+        </div>
 
-    <form action="{% url 'wagtailusers_users:add' %}" method="POST" novalidate{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
-        <div class="tab-content">
-            {% csrf_token %}
-            <section id="account" class="active nice-padding">
-                <ul class="fields">
-                    {% block fields %}
-                        {% if form.separate_username_field %}
-                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %}
-                        {% endif %}
-                        {% include "wagtailadmin/shared/field_as_li.html" with field=form.email %}
-                        {% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %}
-                        {% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %}
-                        {% block extra_fields %}{% endblock extra_fields %}
-                        {% if form.password1 %}
-                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %}
-                        {% endif %}
-                        {% if form.password2 %}
-                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %}
-                        {% endif %}
-                    {% endblock fields %}
+        <form action="{% url 'wagtailusers_users:add' %}" method="POST" novalidate{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
+            <div class="tab-content nice-padding">
+                {% csrf_token %}
+                <section
+                    id="tab-account"
+                    class="w-tabs__panel"
+                    role="tabpanel"
+                    hidden
+                    aria-labelledby="tab-label-account"
+                >
+                    <ul class="fields">
+                        {% block fields %}
+                            {% if form.separate_username_field %}
+                                {% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %}
+                            {% endif %}
+                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.email %}
+                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %}
+                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %}
+                            {% block extra_fields %}{% endblock extra_fields %}
+                            {% if form.password1 %}
+                                {% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %}
+                            {% endif %}
+                            {% if form.password2 %}
+                                {% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %}
+                            {% endif %}
+                        {% endblock fields %}
 
-                    <li><a href="#roles" class="button lowpriority tab-toggle icon icon-arrow-right-after">{% trans "Roles" %}</a></li>
-                </ul>
-            </section>
-            <section id="roles" class="nice-padding">
-                <ul class="fields">
-                    {% include "wagtailadmin/shared/field_as_li.html" with field=form.is_superuser %}
-                    {% include "wagtailadmin/shared/field_as_li.html" with field=form.groups %}
-                    <li><button class="button">{% trans "Add user" %}</button></li>
-                </ul>
-            </section>
-        </div>
-    </form>
+                        <li><a href="#tab-roles" role="tab" class="button lowpriority icon icon-arrow-right-after">{% trans "Roles" %}</a></li>
+                    </ul>
+                </section>
+                <section
+                    id="tab-roles"
+                    class="w-tabs__panel"
+                    role="tabpanel"
+                    hidden
+                    aria-labelledby="tab-label-roles"
+                >
+                    <ul class="fields">
+                        {% include "wagtailadmin/shared/field_as_li.html" with field=form.is_superuser %}
+                        {% include "wagtailadmin/shared/field_as_li.html" with field=form.groups %}
+                        <li><button class="button">{% trans "Add user" %}</button></li>
+                    </ul>
+                </section>
+            </div>
+        </form>
+    </div>
 {% endblock %}
 
 {% block extra_css %}

+ 70 - 52
wagtail/users/templates/wagtailusers/users/edit.html

@@ -1,67 +1,85 @@
 {% extends "wagtailadmin/base.html" %}
 {% load wagtailimages_tags %}
 {% load i18n %}
-{% block titletag %}{% trans "Editing" %} {{ user.get_username}}{% endblock %}
+{% block titletag %}{% trans "Editing" %} {{ user.get_username }}{% endblock %}
 {% block content %}
 
     {% trans "Editing" as editing_str %}
-    {% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=user.get_username merged=1 tabbed=1 icon="user" %}
+    {% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=user.get_username merged=1 icon="user" %}
 
-    <ul class="tab-nav merged" data-tab-nav>
-        <li class="active"><a href="#account">{% trans "Account" %}</a></li>
-        <li><a href="#roles">{% trans "Roles" %}</a></li>
-    </ul>
+    <div class="w-tabs" data-tabs data-tabs-animate>
+        <div class="w-tabs__wrapper">
+            <div role="tablist" class="w-tabs__list nice-padding">
+                {% trans "Account" as account_text %}
+                {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='account' title=account_text %}
+                {% trans "Roles" as roles_text %}
+                {% include 'wagtailadmin/shared/tabs/tab_nav_link.html' with tab_id='roles' title=roles_text %}
+            </div>
+        </div>
 
-    <form action="{% url 'wagtailusers_users:edit' user.pk %}" method="POST" novalidate{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
-        <div class="tab-content">
-            {% csrf_token %}
+        <form action="{% url 'wagtailusers_users:edit' user.pk %}" method="POST" novalidate{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
+            <div class="tab-content nice-padding">
+                {% csrf_token %}
 
-            <section id="account" class="active nice-padding">
-                <ul class="fields">
-                    {% block fields %}
-                        {% if form.separate_username_field %}
-                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %}
-                        {% endif %}
-                        {% include "wagtailadmin/shared/field_as_li.html" with field=form.email %}
-                        {% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %}
-                        {% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %}
-                        {% block extra_fields %}{% endblock extra_fields %}
-                        {% if form.password1 %}
-                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %}
-                        {% endif %}
-                        {% if form.password2 %}
-                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %}
-                        {% endif %}
-                        {% if form.is_active %}
-                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.is_active %}
-                        {% endif %}
+                <section
+                    id="tab-account"
+                    class="w-tabs__panel"
+                    role="tabpanel"
+                    hidden
+                    aria-labelledby="tab-label-account"
+                >
+                    <ul class="fields">
+                        {% block fields %}
+                            {% if form.separate_username_field %}
+                                {% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %}
+                            {% endif %}
+                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.email %}
+                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %}
+                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %}
+                            {% block extra_fields %}{% endblock extra_fields %}
+                            {% if form.password1 %}
+                                {% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %}
+                            {% endif %}
+                            {% if form.password2 %}
+                                {% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %}
+                            {% endif %}
+                            {% if form.is_active %}
+                                {% include "wagtailadmin/shared/field_as_li.html" with field=form.is_active %}
+                            {% endif %}
 
-                    {% endblock fields %}
-                    <li>
-                        <input type="submit" value="{% trans 'Save' %}" class="button" />
-                        {% if can_delete %}
-                            <a href="{% url 'wagtailusers_users:delete' user.pk %}" class="button button-secondary no">{% trans "Delete user" %}</a>
+                        {% endblock fields %}
+                        <li>
+                            <input type="submit" value="{% trans 'Save' %}" class="button"/>
+                            {% if can_delete %}
+                                <a href="{% url 'wagtailusers_users:delete' user.pk %}" class="button button-secondary no">{% trans "Delete user" %}</a>
+                            {% endif %}
+                        </li>
+                    </ul>
+                </section>
+                <section
+                    id="tab-roles"
+                    class="w-tabs__panel"
+                    role="tabpanel"
+                    hidden
+                    aria-labelledby="tab-label-roles"
+                >
+                    <ul class="fields">
+                        {% if form.is_superuser %}
+                            {% include "wagtailadmin/shared/field_as_li.html" with field=form.is_superuser %}
                         {% endif %}
-                    </li>
-                </ul>
-            </section>
-            <section id="roles" class="nice-padding">
-                <ul class="fields">
-                    {% if form.is_superuser %}
-                        {% include "wagtailadmin/shared/field_as_li.html" with field=form.is_superuser %}
-                    {% endif %}
 
-                    {% include "wagtailadmin/shared/field_as_li.html" with field=form.groups %}
-                    <li>
-                        <input type="submit" value="{% trans 'Save' %}" class="button" />
-                        {% if can_delete %}
-                            <a href="{% url 'wagtailusers_users:delete' user.pk %}" class="button button-secondary no">{% trans "Delete user" %}</a>
-                        {% endif %}
-                    </li>
-                </ul>
-            </section>
-        </div>
-    </form>
+                        {% include "wagtailadmin/shared/field_as_li.html" with field=form.groups %}
+                        <li>
+                            <input type="submit" value="{% trans 'Save' %}" class="button"/>
+                            {% if can_delete %}
+                                <a href="{% url 'wagtailusers_users:delete' user.pk %}" class="button button-secondary no">{% trans "Delete user" %}</a>
+                            {% endif %}
+                        </li>
+                    </ul>
+                </section>
+            </div>
+        </form>
+    </div>
 {% endblock %}
 
 {% block extra_css %}