Browse Source

'Prefers-contrast' admin theming (#12348)

Co-authored-by: Victoria Ottah <82820329+Toriasdesign@users.noreply.github.com>
Albina 5 months ago
parent
commit
488c3583b7
37 changed files with 287 additions and 21 deletions
  1. 2 1
      CHANGELOG.txt
  2. 1 0
      CONTRIBUTORS.md
  3. 4 0
      client/scss/components/_a11y-result.scss
  4. 3 1
      client/scss/components/_button.scss
  5. 1 5
      client/scss/components/_chooser.scss
  6. 1 1
      client/scss/components/_dropdown.scss
  7. 1 0
      client/scss/components/_filters.scss
  8. 2 1
      client/scss/components/_form-side.scss
  9. 4 0
      client/scss/components/_header.scss
  10. 4 0
      client/scss/components/_messages.scss
  11. 1 0
      client/scss/components/_panel.scss
  12. 4 0
      client/scss/components/_preview-panel.scss
  13. 1 0
      client/scss/components/forms/_input-base.scss
  14. 60 0
      client/scss/tools/_mixins.general.scss
  15. 1 0
      client/src/components/CommentApp/components/Comment/style.scss
  16. 2 0
      client/src/components/CommentApp/components/CommentHeader/style.scss
  17. 14 0
      client/src/components/Draftail/Draftail.scss
  18. 1 0
      client/src/components/Minimap/CollapseAll.scss
  19. 4 0
      client/src/components/Minimap/MinimapItem.scss
  20. 3 0
      client/src/components/Sidebar/Sidebar.scss
  21. 3 1
      client/src/components/Sidebar/Sidebar.tsx
  22. 3 1
      client/src/components/Sidebar/__snapshots__/Sidebar.test.js.snap
  23. 60 0
      client/src/tokens/colorThemes.js
  24. 14 0
      client/src/tokens/colorVariables.test.js
  25. 10 0
      client/src/tokens/colors.js
  26. 7 0
      client/tailwind.config.js
  27. 7 0
      docs/releases/6.3.md
  28. 1 1
      wagtail/admin/forms/account.py
  29. 1 1
      wagtail/admin/templates/wagtailadmin/pages/listing/_page_header_buttons.html
  30. 1 1
      wagtail/admin/templates/wagtailadmin/shared/headers/slim_header.html
  31. 4 1
      wagtail/admin/templates/wagtailadmin/shared/page_status_tag_new.html
  32. 7 1
      wagtail/admin/templatetags/wagtailadmin_tags.py
  33. 17 1
      wagtail/admin/tests/test_account_management.py
  34. 2 2
      wagtail/admin/tests/test_audit_log.py
  35. 2 2
      wagtail/admin/tests/test_views.py
  36. 23 0
      wagtail/users/migrations/0014_userprofile_contrast.py
  37. 11 0
      wagtail/users/models.py

+ 2 - 1
CHANGELOG.txt

@@ -20,8 +20,9 @@ Changelog
  * Fire `copy_for_translation_done` signal when copying translatable models as well as pages (Coen van der Kamp)
  * Fire `copy_for_translation_done` signal when copying translatable models as well as pages (Coen van der Kamp)
  * Add support for an image `description` field across all images, to better support accessible image descriptions (Chiemezuo Akujobi)
  * Add support for an image `description` field across all images, to better support accessible image descriptions (Chiemezuo Akujobi)
  * Prompt the user about unsaved changes when editing snippets (Sage Abdullah)
  * Prompt the user about unsaved changes when editing snippets (Sage Abdullah)
- * Implement incremental dashboard design enhancements (Albina Starykova)
+ * Implement incremental dashboard design enhancements (Albina Starykova, Ben Enright)
  * Add support for specifying different preview modes to the "View draft" URL for pages (Robin Varghese)
  * Add support for specifying different preview modes to the "View draft" URL for pages (Robin Varghese)
+ * Add a new enhanced contrast admin theming option for the admin interface (Albina Starykova, Victoria Ottah)
  * Fix: Prevent page type business rules from blocking reordering of pages (Andy Babic, Sage Abdullah)
  * Fix: Prevent page type business rules from blocking reordering of pages (Andy Babic, Sage Abdullah)
  * Fix: Improve layout of object permissions table (Sage Abdullah)
  * Fix: Improve layout of object permissions table (Sage Abdullah)
  * Fix: Fix typo in aria-label attribute of page explorer navigation link (Sébastien Corbin)
  * Fix: Fix typo in aria-label attribute of page explorer navigation link (Sébastien Corbin)

+ 1 - 0
CONTRIBUTORS.md

@@ -840,6 +840,7 @@
 * Gabriel Getzie
 * Gabriel Getzie
 * Rohit Singh
 * Rohit Singh
 * Robin Varghese
 * Robin Varghese
+* Victoria Ottah
 
 
 ## Translators
 ## Translators
 
 

+ 4 - 0
client/scss/components/_a11y-result.scss

@@ -8,6 +8,10 @@
   .form-side--checks & {
   .form-side--checks & {
     display: block;
     display: block;
   }
   }
+
+  @include more-contrast() {
+    border-color: theme('colors.border-furniture-more-contrast');
+  }
 }
 }
 
 
 .w-a11y-result__header {
 .w-a11y-result__header {

+ 3 - 1
client/scss/components/_button.scss

@@ -321,11 +321,13 @@
 }
 }
 
 
 .w-header-button {
 .w-header-button {
+  @include more-contrast-interactive();
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
   gap: theme('spacing.1');
   gap: theme('spacing.1');
-  height: theme('spacing.7');
+  height: theme('spacing.8');
+  min-width: theme('spacing.8');
   appearance: none;
   appearance: none;
   background-color: initial;
   background-color: initial;
   border: 1px solid transparent;
   border: 1px solid transparent;

+ 1 - 5
client/scss/components/_chooser.scss

@@ -15,6 +15,7 @@ $preview-size: 2.625rem; // 42px
 // Very subdued button style specifically for choosers, as there can be a lot of
 // Very subdued button style specifically for choosers, as there can be a lot of
 // chooser fields left unused on a page editing form.
 // chooser fields left unused on a page editing form.
 .button.chooser__choose-button {
 .button.chooser__choose-button {
+  @include more-contrast-interactive();
   @apply w-label-3;
   @apply w-label-3;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -81,11 +82,6 @@ $preview-size: 2.625rem; // 42px
   width: auto;
   width: auto;
 }
 }
 
 
-.chooser .w-dropdown__toggle--icon {
-  width: $preview-size;
-  height: $preview-size;
-}
-
 // Display these as inline block so that action icons such as comments can appear as close as possible
 // Display these as inline block so that action icons such as comments can appear as close as possible
 .w-field--admin_task_chooser,
 .w-field--admin_task_chooser,
 .w-field--admin_page_chooser,
 .w-field--admin_page_chooser,

+ 1 - 1
client/scss/components/_dropdown.scss

@@ -3,7 +3,7 @@
 }
 }
 
 
 .w-dropdown__toggle--icon {
 .w-dropdown__toggle--icon {
-  @apply w-w-8 w-h-8;
+  @apply w-w-8 w-h-8 more-contrast:w-p-0 more-contrast:w-border more-contrast:w-rounded-sm more-contrast:w-border-border-interactive-more-contrast hover:more-contrast:w-border-border-interactive-more-contrast-hover;
 }
 }
 
 
 .w-dropdown__toggle-icon {
 .w-dropdown__toggle-icon {

+ 1 - 0
client/scss/components/_filters.scss

@@ -25,6 +25,7 @@
 }
 }
 
 
 .w-filter-button {
 .w-filter-button {
+  @include more-contrast-interactive();
   position: relative;
   position: relative;
   width: theme('spacing.10');
   width: theme('spacing.10');
   height: theme('spacing.10');
   height: theme('spacing.10');

+ 2 - 1
client/scss/components/_form-side.scss

@@ -36,7 +36,8 @@
       sm:w-max-w-[22.5rem]
       sm:w-max-w-[22.5rem]
       md:w-max-w-[35.937rem]
       md:w-max-w-[35.937rem]
       lg:w-max-w-[31.25rem]
       lg:w-max-w-[31.25rem]
-      xl:w-max-w-[46.875rem];
+      xl:w-max-w-[46.875rem]
+      more-contrast:w-border-border-furniture-more-contrast;
   z-index: calc(theme('zIndex.header') - 10);
   z-index: calc(theme('zIndex.header') - 10);
   width: var(--side-panel-width, 100%);
   width: var(--side-panel-width, 100%);
 
 

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

@@ -151,6 +151,10 @@
 }
 }
 
 
 .w-slim-header {
 .w-slim-header {
+  @include more-contrast() {
+    border-color: theme('colors.border-furniture-more-contrast');
+  }
+
   &__search-form {
   &__search-form {
     @apply w-mx-2 w-flex w-items-center w-gap-2;
     @apply w-mx-2 w-flex w-items-center w-gap-2;
   }
   }

+ 4 - 0
client/scss/components/_messages.scss

@@ -58,6 +58,10 @@
   .warning {
   .warning {
     background-color: theme('colors.warning.100');
     background-color: theme('colors.warning.100');
     color: theme('colors.grey.600');
     color: theme('colors.grey.600');
+
+    @include more-contrast() {
+      background-color: theme('colors.warning.75');
+    }
   }
   }
 
 
   .info {
   .info {

+ 1 - 0
client/scss/components/_panel.scss

@@ -61,6 +61,7 @@ $header-button-size: theme('spacing.6');
 .w-panel__toggle,
 .w-panel__toggle,
 .w-panel__controls .button.button--icon {
 .w-panel__controls .button.button--icon {
   @include show-focus-outline-inside();
   @include show-focus-outline-inside();
+  @include more-contrast-interactive();
   display: inline-grid;
   display: inline-grid;
   justify-content: center;
   justify-content: center;
   align-content: center;
   align-content: center;

+ 4 - 0
client/scss/components/_preview-panel.scss

@@ -59,6 +59,10 @@
     gap: 0.75rem;
     gap: 0.75rem;
     padding-bottom: 1rem;
     padding-bottom: 1rem;
     margin-bottom: 1rem;
     margin-bottom: 1rem;
+
+    @include more-contrast() {
+      border-color: theme('colors.border-furniture-more-contrast');
+    }
   }
   }
 
 
   &__size-button {
   &__size-button {

+ 1 - 0
client/scss/components/forms/_input-base.scss

@@ -3,6 +3,7 @@
  * Text input, textarea, checkbox, radio, select, etc.
  * Text input, textarea, checkbox, radio, select, etc.
  */
  */
 @mixin input-base() {
 @mixin input-base() {
+  @include more-contrast-interactive();
   appearance: none;
   appearance: none;
   border-radius: theme('borderRadius.DEFAULT');
   border-radius: theme('borderRadius.DEFAULT');
   color: theme('colors.text-context');
   color: theme('colors.text-context');

+ 60 - 0
client/scss/tools/_mixins.general.scss

@@ -93,6 +93,21 @@
   }
   }
 }
 }
 
 
+/**
+ * Apply styles for enhanced contrast theming.
+ */
+@mixin more-contrast() {
+  .w-contrast-more & {
+    @content;
+  }
+
+  @media (prefers-contrast: more) {
+    .w-contrast-system & {
+      @content;
+    }
+  }
+}
+
 /**
 /**
  * Apply styles for the light theme only.
  * Apply styles for the light theme only.
  */
  */
@@ -107,3 +122,48 @@
     }
     }
   }
   }
 }
 }
+
+/**
+ * Apply styles for the dark theme with increased contrast.
+ */
+@mixin dark-theme-more-contrast() {
+  .w-theme-dark.w-contrast-more & {
+    @content;
+  }
+
+  @media (prefers-color-scheme: dark) {
+    .w-theme-system.w-contrast-more & {
+      @content;
+    }
+  }
+
+  @media (prefers-contrast: more) {
+    .w-theme-dark.w-contrast-system & {
+      @content;
+    }
+  }
+
+  @media (prefers-color-scheme: dark) and (prefers-contrast: more) {
+    .w-theme-system.w-contrast-system & {
+      @content;
+    }
+  }
+}
+
+/**
+* Increased contrast theme styles for interactive components
+*/
+@mixin more-contrast-interactive() {
+  @include more-contrast() {
+    border: 1px solid theme('colors.border-interactive-more-contrast');
+
+    &:hover {
+      border-color: theme('colors.border-interactive-more-contrast-hover');
+    }
+
+    &[disabled],
+    &[disabled]:hover {
+      border-style: dashed;
+    }
+  }
+}

+ 1 - 0
client/src/components/CommentApp/components/Comment/style.scss

@@ -1,5 +1,6 @@
 .comment {
 .comment {
   @include box;
   @include box;
+  @include more-contrast-interactive();
 
 
   width: 300px;
   width: 300px;
   display: block;
   display: block;

+ 2 - 0
client/src/components/CommentApp/components/CommentHeader/style.scss

@@ -40,6 +40,8 @@
 
 
     > button,
     > button,
     > details > summary {
     > details > summary {
+      @include more-contrast-interactive();
+      border-radius: theme('borderRadius.sm');
       list-style-type: none; // Hides triangle on Firefox
       list-style-type: none; // Hides triangle on Firefox
       width: 30px;
       width: 30px;
       height: 30px;
       height: 30px;

+ 14 - 0
client/src/components/Draftail/Draftail.scss

@@ -101,6 +101,11 @@ $draftail-editor-font-family: theme('fontFamily.sans');
   background-color: $draftail-editor-background;
   background-color: $draftail-editor-background;
   color: $draftail-placeholder-text;
   color: $draftail-placeholder-text;
 
 
+  &--pin {
+    display: flex;
+    flex-wrap: wrap;
+  }
+
   .Draftail-Editor--focus & {
   .Draftail-Editor--focus & {
     color: $draftail-editor-text;
     color: $draftail-editor-text;
     top: calc(theme('spacing.slim-header') * 2);
     top: calc(theme('spacing.slim-header') * 2);
@@ -205,11 +210,19 @@ $draftail-editor-font-family: theme('fontFamily.sans');
   }
   }
 }
 }
 
 
+.Draftail-ToolbarGroup {
+  display: flex;
+}
+
 .Draftail-ToolbarGroup::before {
 .Draftail-ToolbarGroup::before {
   display: none;
   display: none;
 }
 }
 
 
 .Draftail-ToolbarButton {
 .Draftail-ToolbarButton {
+  @include more-contrast-interactive();
+  display: flex;
+  align-items: center;
+  justify-content: center;
   height: 1.875rem;
   height: 1.875rem;
   min-width: 1.875rem;
   min-width: 1.875rem;
   padding: 0;
   padding: 0;
@@ -257,6 +270,7 @@ $draftail-editor-font-family: theme('fontFamily.sans');
   .Draftail-Toolbar & {
   .Draftail-Toolbar & {
     border-color: theme('colors.border-field-default');
     border-color: theme('colors.border-field-default');
     background-color: theme('colors.surface-page');
     background-color: theme('colors.surface-page');
+    color: currentColor;
     border-top-width: 0;
     border-top-width: 0;
     border-inline-end-width: 0;
     border-inline-end-width: 0;
 
 

+ 1 - 0
client/src/components/Minimap/CollapseAll.scss

@@ -1,4 +1,5 @@
 .w-minimap__collapse-all {
 .w-minimap__collapse-all {
+  @include more-contrast-interactive();
   display: none;
   display: none;
   // Keep the icon at a stable position and reduce the amount of shifting of the button.
   // Keep the icon at a stable position and reduce the amount of shifting of the button.
   min-width: 110px;
   min-width: 110px;

+ 4 - 0
client/src/components/Minimap/MinimapItem.scss

@@ -35,6 +35,10 @@
     @media (forced-colors: active) {
     @media (forced-colors: active) {
       border-inline-start-width: 3px;
       border-inline-start-width: 3px;
     }
     }
+
+    @include more-contrast() {
+      border-inline-start-width: 3px;
+    }
   }
   }
 
 
   &:hover {
   &:hover {

+ 3 - 0
client/src/components/Sidebar/Sidebar.scss

@@ -45,6 +45,9 @@ $sidebar-toggle-size: 35px;
   @media (forced-colors: active) {
   @media (forced-colors: active) {
     border-inline-end: 1px solid transparent;
     border-inline-end: 1px solid transparent;
   }
   }
+  @include dark-theme-more-contrast() {
+    border-inline-end: 1px solid theme('colors.border-furniture-more-contrast');
+  }
 
 
   .icon--menuitem {
   .icon--menuitem {
     width: 1rem;
     width: 1rem;

+ 3 - 1
client/src/components/Sidebar/Sidebar.tsx

@@ -230,7 +230,9 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
                 w-items-center
                 w-items-center
                 hover:w-bg-surface-menu-item-active
                 hover:w-bg-surface-menu-item-active
                 hover:text-white
                 hover:text-white
-                hover:opacity-100`}
+                hover:opacity-100
+                more-contrast:w-border-border-interactive-more-contrast-dark-bg
+                hover:more-contrast:w-border-border-interactive-more-contrast-dark-bg-hover`}
             >
             >
               <Icon
               <Icon
                 name="expand-right"
                 name="expand-right"

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

@@ -35,7 +35,9 @@ exports[`Sidebar should render with the minimum required props 1`] = `
                 w-items-center
                 w-items-center
                 hover:w-bg-surface-menu-item-active
                 hover:w-bg-surface-menu-item-active
                 hover:text-white
                 hover:text-white
-                hover:opacity-100"
+                hover:opacity-100
+                more-contrast:w-border-border-interactive-more-contrast-dark-bg
+                hover:more-contrast:w-border-border-interactive-more-contrast-dark-bg-hover"
           onClick={[Function]}
           onClick={[Function]}
           type="button"
           type="button"
         >
         >

+ 60 - 0
client/src/tokens/colorThemes.js

@@ -295,6 +295,36 @@ const light = [
         textUtility: 'w-text-border-button-outline-hover',
         textUtility: 'w-text-border-button-outline-hover',
         cssVariable: '--w-color-border-button-outline-hover',
         cssVariable: '--w-color-border-button-outline-hover',
       },
       },
+      'border-interactive-more-contrast': {
+        value: 'var(--w-color-grey-500)',
+        bgUtility: 'w-bg-border-interactive-more-contrast',
+        textUtility: 'w-text-border-interactive-more-contrast',
+        cssVariable: '--w-color-border-interactive-more-contrast',
+      },
+      'border-interactive-more-contrast-hover': {
+        value: 'var(--w-color-black)',
+        bgUtility: 'w-bg-border-interactive-more-contrast-hover',
+        textUtility: 'w-text-border-interactive-more-contrast-hover',
+        cssVariable: '--w-color-border-interactive-more-contrast-hover',
+      },
+      'border-interactive-more-contrast-dark-bg': {
+        value: 'var(--w-color-grey-150)',
+        bgUtility: 'w-bg-border-interactive-more-contrast-dark-bg',
+        textUtility: 'w-text-border-interactive-more-contrast-dark-bg',
+        cssVariable: '--w-color-border-interactive-more-contrast-dark-bg',
+      },
+      'border-interactive-more-contrast-dark-bg-hover': {
+        value: 'var(--w-color-white)',
+        bgUtility: 'w-bg-border-interactive-more-contrast-dark-bg-hover',
+        textUtility: 'w-text-border-interactive-more-contrast-dark-bg-hover',
+        cssVariable: '--w-color-border-interactive-more-contrast-dark-bg-hover',
+      },
+      'border-furniture-more-contrast': {
+        value: 'var(--w-color-grey-200)',
+        bgUtility: 'w-bg-border-furniture-more-contrast',
+        textUtility: 'w-text-border-furniture-more-contrast',
+        cssVariable: '--w-color-border-furniture-more-contrast',
+      },
     },
     },
   },
   },
   {
   {
@@ -583,6 +613,36 @@ const dark = [
         textUtility: 'w-text-border-button-outline-hover',
         textUtility: 'w-text-border-button-outline-hover',
         cssVariable: '--w-color-border-button-outline-hover',
         cssVariable: '--w-color-border-button-outline-hover',
       },
       },
+      'border-interactive-more-contrast': {
+        value: 'var(--w-color-grey-150)',
+        bgUtility: 'w-bg-border-interactive-more-contrast',
+        textUtility: 'w-text-border-interactive-more-contrast',
+        cssVariable: '--w-color-border-interactive-more-contrast',
+      },
+      'border-interactive-more-contrast-hover': {
+        value: 'var(--w-color-white)',
+        bgUtility: 'w-bg-border-interactive-more-contrast-hover',
+        textUtility: 'w-text-border-interactive-more-contrast-hover',
+        cssVariable: '--w-color-border-interactive-more-contrast-hover',
+      },
+      'border-interactive-more-contrast-dark-bg': {
+        value: 'var(--w-color-grey-150)',
+        bgUtility: 'w-bg-border-interactive-more-contrast-dark-bg',
+        textUtility: 'w-text-border-interactive-more-contrast-dark-bg',
+        cssVariable: '--w-color-border-interactive-more-contrast-dark-bg',
+      },
+      'border-interactive-more-contrast-dark-bg-hover': {
+        value: 'var(--w-color-white)',
+        bgUtility: 'w-bg-border-interactive-more-contrast-dark-bg-hover',
+        textUtility: 'w-text-border-interactive-more-contrast-dark-bg-hover',
+        cssVariable: '--w-color-border-interactive-more-contrast-dark-bg-hover',
+      },
+      'border-furniture-more-contrast': {
+        value: 'var(--w-color-grey-400)',
+        bgUtility: 'w-bg-border-furniture-more-contrast',
+        textUtility: 'w-text-border-furniture-more-contrast',
+        cssVariable: '--w-color-border-furniture-more-contrast',
+      },
     },
     },
   },
   },
   {
   {

+ 14 - 0
client/src/tokens/colorVariables.test.js

@@ -143,6 +143,10 @@ describe('generateColorVariables', () => {
         "--w-color-warning-50-hue": "calc(var(--w-color-warning-100-hue) - 2.3)",
         "--w-color-warning-50-hue": "calc(var(--w-color-warning-100-hue) - 2.3)",
         "--w-color-warning-50-lightness": "calc(var(--w-color-warning-100-lightness) + 41.8%)",
         "--w-color-warning-50-lightness": "calc(var(--w-color-warning-100-lightness) + 41.8%)",
         "--w-color-warning-50-saturation": "calc(var(--w-color-warning-100-saturation) - 21.3%)",
         "--w-color-warning-50-saturation": "calc(var(--w-color-warning-100-saturation) - 21.3%)",
+        "--w-color-warning-75": "hsl(var(--w-color-warning-75-hue) var(--w-color-warning-75-saturation) var(--w-color-warning-75-lightness))",
+        "--w-color-warning-75-hue": "calc(var(--w-color-warning-100-hue) + 0.7)",
+        "--w-color-warning-75-lightness": "calc(var(--w-color-warning-100-lightness) + 23.4%)",
+        "--w-color-warning-75-saturation": "calc(var(--w-color-warning-100-saturation) - 2.8%)",
         "--w-color-white": "hsl(var(--w-color-white-hue) var(--w-color-white-saturation) var(--w-color-white-lightness))",
         "--w-color-white": "hsl(var(--w-color-white-hue) var(--w-color-white-saturation) var(--w-color-white-lightness))",
         "--w-color-white-hue": "0",
         "--w-color-white-hue": "0",
         "--w-color-white-lightness": "100%",
         "--w-color-white-lightness": "100%",
@@ -200,6 +204,11 @@ describe('generateThemeColorVariables', () => {
         "--w-color-border-field-hover": "var(--w-color-grey-200)",
         "--w-color-border-field-hover": "var(--w-color-grey-200)",
         "--w-color-border-field-inactive": "var(--w-color-grey-150)",
         "--w-color-border-field-inactive": "var(--w-color-grey-150)",
         "--w-color-border-furniture": "var(--w-color-grey-100)",
         "--w-color-border-furniture": "var(--w-color-grey-100)",
+        "--w-color-border-furniture-more-contrast": "var(--w-color-grey-200)",
+        "--w-color-border-interactive-more-contrast": "var(--w-color-grey-500)",
+        "--w-color-border-interactive-more-contrast-dark-bg": "var(--w-color-grey-150)",
+        "--w-color-border-interactive-more-contrast-dark-bg-hover": "var(--w-color-white)",
+        "--w-color-border-interactive-more-contrast-hover": "var(--w-color-black)",
         "--w-color-box-shadow-md": "var(--w-color-black-25)",
         "--w-color-box-shadow-md": "var(--w-color-black-25)",
         "--w-color-focus": "#00A885",
         "--w-color-focus": "#00A885",
         "--w-color-icon-primary": "var(--w-color-primary)",
         "--w-color-icon-primary": "var(--w-color-primary)",
@@ -252,6 +261,11 @@ describe('generateThemeColorVariables', () => {
         "--w-color-border-field-hover": "var(--w-color-grey-200)",
         "--w-color-border-field-hover": "var(--w-color-grey-200)",
         "--w-color-border-field-inactive": "var(--w-color-grey-500)",
         "--w-color-border-field-inactive": "var(--w-color-grey-500)",
         "--w-color-border-furniture": "var(--w-color-grey-500)",
         "--w-color-border-furniture": "var(--w-color-grey-500)",
+        "--w-color-border-furniture-more-contrast": "var(--w-color-grey-400)",
+        "--w-color-border-interactive-more-contrast": "var(--w-color-grey-150)",
+        "--w-color-border-interactive-more-contrast-dark-bg": "var(--w-color-grey-150)",
+        "--w-color-border-interactive-more-contrast-dark-bg-hover": "var(--w-color-white)",
+        "--w-color-border-interactive-more-contrast-hover": "var(--w-color-white)",
         "--w-color-box-shadow-md": "var(--w-color-black-50)",
         "--w-color-box-shadow-md": "var(--w-color-black-50)",
         "--w-color-focus": "#00A885",
         "--w-color-focus": "#00A885",
         "--w-color-icon-primary": "var(--w-color-grey-150)",
         "--w-color-icon-primary": "var(--w-color-grey-150)",

+ 10 - 0
client/src/tokens/colors.js

@@ -268,6 +268,16 @@ const staticColors = {
       usage: 'Background and icons for potentially dangerous states',
       usage: 'Background and icons for potentially dangerous states',
       contrastText: 'primary',
       contrastText: 'primary',
     },
     },
+    75: {
+      hex: '#FDD074',
+      hsl: 'hsl(40.3, 97.2%, 72.4%)',
+      bgUtility: 'w-bg-warning-75',
+      textUtility: 'w-text-warning-75',
+      cssVariable: '--w-color-warning-75',
+      usage:
+        'Background only, for potentially dangerous states, in enhanced-contrast theme',
+      contrastText: 'primary',
+    },
     50: {
     50: {
       hex: '#FFF5D8',
       hex: '#FFF5D8',
       hsl: 'hsl(37.3 78.7% 90.8%)',
       hsl: 'hsl(37.3 78.7% 90.8%)',

+ 7 - 0
client/tailwind.config.js

@@ -190,6 +190,13 @@ module.exports = {
     plugin(({ addVariant }) => {
     plugin(({ addVariant }) => {
       addVariant('expanded', '&[aria-expanded=true]');
       addVariant('expanded', '&[aria-expanded=true]');
     }),
     }),
+    /** Support for increased contrast theme */
+    plugin(({ addVariant }) => {
+      addVariant('more-contrast', [
+        '.contrast-more &',
+        '@media (prefers-contrast: more) { .contrast-system & }',
+      ]);
+    }),
   ],
   ],
   corePlugins: {
   corePlugins: {
     ...vanillaRTL.disabledCorePlugins,
     ...vanillaRTL.disabledCorePlugins,

+ 7 - 0
docs/releases/6.3.md

@@ -25,6 +25,13 @@ The Wagtail dashboard design evolves towards providing more information and navi
 
 
 This feature was developed by Albina Starykova based on designs by Ben Enright.
 This feature was developed by Albina Starykova based on designs by Ben Enright.
 
 
+### Enhanced contrast admin theme
+
+CMS users can now control the level of contrast of UI elements in the admin interface.
+This new customization is designed for partially sighted users, complementing existing support for a dark theme and Windows Contrast Themes.
+The new "More contrast" theming can be enabled in account preferences, or will otherwise be derived from operating system preferences.
+
+This feature was designed thanks to feedback from our blind and partially sighted users, and was developed by Albina Starykova based on design input from Victoria Ottah.
 
 
 ### Other features
 ### Other features
 
 

+ 1 - 1
wagtail/admin/forms/account.py

@@ -140,4 +140,4 @@ class AvatarPreferencesForm(forms.ModelForm):
 class ThemePreferencesForm(forms.ModelForm):
 class ThemePreferencesForm(forms.ModelForm):
     class Meta:
     class Meta:
         model = UserProfile
         model = UserProfile
-        fields = ["theme", "density"]
+        fields = ["theme", "contrast", "density"]

+ 1 - 1
wagtail/admin/templates/wagtailadmin/pages/listing/_page_header_buttons.html

@@ -2,7 +2,7 @@
 
 
 {% trans "Actions" as title %}
 {% trans "Actions" as title %}
 <nav aria-label="{{ title }}">
 <nav aria-label="{{ title }}">
-    {% dropdown toggle_icon="dots-horizontal" toggle_aria_label=title toggle_classname="w-p-0 w-w-8 w-h-slim-header hover:w-scale-110 w-transition w-outline-offset-inside w-relative w-z-30" toggle_tooltip_offset="[0, -2]" %}
+    {% dropdown toggle_icon="dots-horizontal" toggle_aria_label=title toggle_classname="w-p-0 w-outline-offset-inside w-relative w-z-30" toggle_tooltip_offset="[0, -2]" %}
         {% block content %}
         {% block content %}
             {% include "wagtailadmin/pages/listing/_dropdown_items.html" with buttons=buttons only %}
             {% include "wagtailadmin/pages/listing/_dropdown_items.html" with buttons=buttons only %}
         {% endblock %}
         {% endblock %}

+ 1 - 1
wagtail/admin/templates/wagtailadmin/shared/headers/slim_header.html

@@ -19,7 +19,7 @@
 
 
 {% endcomment %}
 {% endcomment %}
 {% fragment as nav_icon_classes %}w-w-4 w-h-4 group-hover:w-transform group-hover:w-scale-110{% endfragment %}
 {% fragment as nav_icon_classes %}w-w-4 w-h-4 group-hover:w-transform group-hover:w-scale-110{% endfragment %}
-{% fragment as nav_icon_button_classes %}w-w-slim-header w-h-slim-header w-bg-transparent w-border-transparent w-box-border w-py-3 w-px-3 w-flex w-justify-center w-items-center w-outline-offset-inside w-text-text-meta w-transition w-group hover:w-text-text-label focus:w-text-text-label expanded:w-text-text-label expanded:w-border-y-2 expanded:w-border-b-current w-shrink-0{% endfragment %}
+{% fragment as nav_icon_button_classes %}w-w-slim-header w-h-slim-header w-bg-transparent w-border-transparent w-box-border w-py-3 w-px-3 w-flex w-justify-center w-items-center w-outline-offset-inside w-text-text-meta w-transition w-group hover:w-text-text-label focus:w-text-text-label expanded:w-text-text-label expanded:w-border-y-2 expanded:w-border-b-current w-shrink-0 more-contrast:w-border more-contrast:w-border-border-interactive-more-contrast hover:more-contrast:w-border-border-interactive-more-contrast-hover{% endfragment %}
 {% fragment as nav_icon_counter_classes %}-w-mr-3 w-py-0.5 w-px-[0.325rem] w-translate-y-[-8px] rtl:w-translate-x-[4px] w-translate-x-[-4px] w-text-[0.5625rem] w-font-bold w-text-text-button w-border w-border-surface-page w-rounded-[1rem]{% endfragment %}
 {% fragment as nav_icon_counter_classes %}-w-mr-3 w-py-0.5 w-px-[0.325rem] w-translate-y-[-8px] rtl:w-translate-x-[4px] w-translate-x-[-4px] w-text-[0.5625rem] w-font-bold w-text-text-button w-border w-border-surface-page w-rounded-[1rem]{% endfragment %}
 {# Z index 99 to ensure header is always above  #}
 {# Z index 99 to ensure header is always above  #}
 <div class="w-sticky w-top-0 w-z-header">
 <div class="w-sticky w-top-0 w-z-header">

+ 4 - 1
wagtail/admin/templates/wagtailadmin/shared/page_status_tag_new.html

@@ -31,7 +31,10 @@
               w-font-semibold
               w-font-semibold
               hover:w-border-surface-menus
               hover:w-border-surface-menus
               hover:w-text-text-label
               hover:w-text-text-label
-              w-transition"
+              w-transition
+              more-contrast:w-border
+              more-contrast:w-border-border-interactive-more-contrast
+              hover:more-contrast:w-border-border-interactive-more-contrast-hover"
        aria-label="{% if is_public %}{% trans 'Visible to all. Visit the live page' %}{% else %}{% trans 'Private. Visit the live page' %}{% endif %}"
        aria-label="{% if is_public %}{% trans 'Visible to all. Visit the live page' %}{% else %}{% trans 'Private. Visit the live page' %}{% endif %}"
        data-controller="w-tooltip"
        data-controller="w-tooltip"
        data-w-tooltip-content-value="{% if is_public %}{% trans 'Visible to all' %}{% else %}{% trans 'Private' %}{% endif %}"
        data-w-tooltip-content-value="{% if is_public %}{% trans 'Visible to all' %}{% else %}{% trans 'Private' %}{% endif %}"

+ 7 - 1
wagtail/admin/templatetags/wagtailadmin_tags.py

@@ -683,12 +683,18 @@ def admin_theme_classname(context):
         if hasattr(user, "wagtail_userprofile")
         if hasattr(user, "wagtail_userprofile")
         else "system"
         else "system"
     )
     )
+    contrast_name = (
+        user.wagtail_userprofile.contrast
+        if hasattr(user, "wagtail_userprofile")
+        else "system"
+    )
     density_name = (
     density_name = (
         user.wagtail_userprofile.density
         user.wagtail_userprofile.density
         if hasattr(user, "wagtail_userprofile")
         if hasattr(user, "wagtail_userprofile")
         else "default"
         else "default"
     )
     )
-    return f"w-theme-{theme_name} w-density-{density_name}"
+    contrast_name = contrast_name.split("_")[0]
+    return f"w-theme-{theme_name} w-density-{density_name} w-contrast-{contrast_name}"
 
 
 
 
 @register.simple_tag
 @register.simple_tag

+ 17 - 1
wagtail/admin/tests/test_account_management.py

@@ -232,6 +232,7 @@ class TestAccountSectionUtilsMixin:
             "locale-current_time_zone": "Europe/London",
             "locale-current_time_zone": "Europe/London",
             "theme-theme": "dark",
             "theme-theme": "dark",
             "theme-density": "default",
             "theme-density": "default",
+            "theme-contrast": "system",
         }
         }
         post_data.update(extra_post_data)
         post_data.update(extra_post_data)
         return self.client.post(reverse("wagtailadmin_account"), post_data)
         return self.client.post(reverse("wagtailadmin_account"), post_data)
@@ -480,7 +481,7 @@ class TestAccountSection(WagtailTestUtils, TestCase, TestAccountSectionUtilsMixi
         response = self.client.get(reverse("wagtailadmin_home"))
         response = self.client.get(reverse("wagtailadmin_home"))
         self.assertContains(
         self.assertContains(
             response,
             response,
-            '<html lang="es" dir="ltr" class="w-theme-dark w-density-default">',
+            '<html lang="es" dir="ltr" class="w-theme-dark w-density-default w-contrast-system">',
         )
         )
 
 
     def test_unset_language_preferences(self):
     def test_unset_language_preferences(self):
@@ -610,6 +611,21 @@ class TestAccountSection(WagtailTestUtils, TestCase, TestAccountSectionUtilsMixi
 
 
         self.assertEqual(profile.theme, "light")
         self.assertEqual(profile.theme, "light")
 
 
+    def test_change_contrast_post(self):
+        response = self.post_form(
+            {
+                "theme-contrast": "more_contrast",
+            }
+        )
+
+        # Check that the user was redirected to the account page
+        self.assertRedirects(response, reverse("wagtailadmin_account"))
+
+        profile = UserProfile.get_for_user(self.user)
+        profile.refresh_from_db()
+
+        self.assertEqual(profile.contrast, "more_contrast")
+
     def test_change_density_post(self):
     def test_change_density_post(self):
         response = self.post_form(
         response = self.post_form(
             {
             {

+ 2 - 2
wagtail/admin/tests/test_audit_log.py

@@ -144,8 +144,8 @@ class TestAuditLogAdmin(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
         )
         )
 
 
         self.assertContains(
         self.assertContains(
-            response, "system", 3
-        )  # create without a user + remove restriction + 1 from unrelated admin color theme
+            response, "system", 4
+        )  # create without a user + remove restriction + 2 from unrelated admin color theme
         self.assertContains(
         self.assertContains(
             response, "the_editor", 9
             response, "the_editor", 9
         )  # 7 entries by editor + 1 in sidebar menu + 1 in filter
         )  # 7 entries by editor + 1 in sidebar menu + 1 in filter

+ 2 - 2
wagtail/admin/tests/test_views.py

@@ -81,7 +81,7 @@ class TestLoginView(WagtailTestUtils, TestCase):
         response = self.client.get(reverse("wagtailadmin_login"))
         response = self.client.get(reverse("wagtailadmin_login"))
         self.assertContains(
         self.assertContains(
             response,
             response,
-            '<html lang="de" dir="ltr" class="w-theme-system w-density-default">',
+            '<html lang="de" dir="ltr" class="w-theme-system w-density-default w-contrast-system">',
         )
         )
 
 
     @override_settings(LANGUAGE_CODE="he")
     @override_settings(LANGUAGE_CODE="he")
@@ -89,7 +89,7 @@ class TestLoginView(WagtailTestUtils, TestCase):
         response = self.client.get(reverse("wagtailadmin_login"))
         response = self.client.get(reverse("wagtailadmin_login"))
         self.assertContains(
         self.assertContains(
             response,
             response,
-            '<html lang="he" dir="rtl" class="w-theme-system w-density-default">',
+            '<html lang="he" dir="rtl" class="w-theme-system w-density-default w-contrast-system">',
         )
         )
 
 
     @override_settings(
     @override_settings(

+ 23 - 0
wagtail/users/migrations/0014_userprofile_contrast.py

@@ -0,0 +1,23 @@
+# Generated by Django 5.0.6 on 2024-09-23 15:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("wagtailusers", "0013_userprofile_density"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="userprofile",
+            name="contrast",
+            field=models.CharField(
+                choices=[("system", "System default"), ("more_contrast", "More contrast")],
+                default="system",
+                max_length=40,
+                verbose_name="contrast",
+            ),
+        ),
+    ]

+ 11 - 0
wagtail/users/models.py

@@ -84,6 +84,17 @@ class UserProfile(models.Model):
         max_length=40,
         max_length=40,
     )
     )
 
 
+    class AdminContrastThemes(models.TextChoices):
+        SYSTEM = "system", _("System default")
+        MORE_CONTRAST = "more_contrast", _("More contrast")
+
+    contrast = models.CharField(
+        verbose_name=_("contrast"),
+        choices=AdminContrastThemes.choices,
+        default=AdminContrastThemes.SYSTEM,
+        max_length=40,
+    )
+
     class AdminDensityThemes(models.TextChoices):
     class AdminDensityThemes(models.TextChoices):
         DEFAULT = "default", _("Default")
         DEFAULT = "default", _("Default")
         SNUG = "snug", _("Snug")
         SNUG = "snug", _("Snug")