Просмотр исходного кода

Rich text toolbar refinements from usability testing. Fix #9281 (#9905)

Thibaud Colas 2 лет назад
Родитель
Сommit
34f6bab633

+ 5 - 0
CHANGELOG.txt

@@ -25,6 +25,8 @@ Changelog
  * Implement latest design for the admin dashboard header (Thibaud Colas, Steven Steinwand)
  * Add base Axe accessibility checker integration within userbar, with error count (Albina Starykova)
  * Allow configuring Axe accessibility checker integration via `construct_wagtail_userbar` hook (Sage Abdullah)
+ * Support pinning and un-pinning the rich text editor toolbar depending on user preference (Thibaud Colas)
+ * Make the rich text block trigger and slash-commands always available regardless of where the cursor is (Thibaud Colas)
  * Fix: Make sure workflow timeline icons are visible in high-contrast mode (Loveth Omokaro)
  * Fix: Ensure authentication forms (login, password reset) have a visible border in Windows high-contrast mode (Loveth Omokaro)
  * Fix: Ensure visual consistency between buttons and links as buttons in Windows high-contrast mode (Albina Starykova)
@@ -63,6 +65,9 @@ Changelog
  * Fix: Resolve issue where workflow and other notification emails would not include the correct tab URL for account notification management (LB (Ben) Johnston)
  * Fix: Use consistent spacing above and below page headers (Thibaud Colas)
  * Fix: Use the correct icon sizes and spacing in slim header (Thibaud Colas)
+ * Fix: Use the correct color for placeholders in rich text fields (Thibaud Colas)
+ * Fix: Prevent obstructing the outline around rich text fields (Thibaud Colas)
+ * Fix: Page editor dropdowns now use indigo backgrounds like elsewhere in the admin interface (Thibaud Colas)
  * Docs: Add custom permissions section to permissions documentation page (Dan Hayden)
  * Docs: Add documentation for how to get started with contributing translations for the Wagtail admin (Ogunbanjo Oluwadamilare)
  * Docs: Officially recommend `fnm` over `nvm` in development documentation (LB (Ben) Johnston)

+ 1 - 0
CONTRIBUTORS.rst

@@ -682,6 +682,7 @@ Contributors
 * Jhonatan Lopes
 * Alex Simpson
 * GLEF1X
+* Nick Lee
 
 Translators
 ===========

+ 54 - 11
client/src/components/Draftail/Draftail.scss

@@ -1,4 +1,5 @@
 $draftail-editor-text: $color-input-text;
+$draftail-placeholder-text: theme('colors.grey.400');
 // w-body-text-large
 $draftail-editor-font-size: theme('fontSize.18');
 $draftail-editor-line-height: theme('lineHeight.normal');
@@ -76,9 +77,14 @@ $draftail-editor-font-family: $font-sans;
 }
 
 .Draftail-Editor {
+  @include input-base();
   // Number used inside a `calc` function, which doesn’t support unitless zero.
   // stylelint-disable-next-line length-zero-no-unit
   --draftail-offset-inline-start: 0px;
+
+  &--focus {
+    outline: $focus-outline-width solid $color-focus-outline;
+  }
 }
 
 .Draftail-Editor__wrapper {
@@ -107,13 +113,37 @@ $draftail-editor-font-family: $font-sans;
 }
 
 .Draftail-Toolbar {
-  border: 1px solid $color-grey-3;
+  border-width: 0;
+  // Remove once we drop support for Safari 14.
+  // stylelint-disable-next-line property-disallowed-list
+  border-bottom-left-radius: 0;
+  border-end-start-radius: 0;
+  // Remove once we drop support for Safari 14.
+  // stylelint-disable-next-line property-disallowed-list
+  border-bottom-right-radius: 0;
+  border-end-end-radius: 0;
+  background-color: $draftail-editor-background;
+  color: $draftail-placeholder-text;
+
+  .Draftail-Editor--focus & {
+    color: $draftail-editor-text;
+    top: calc(theme('spacing.slim-header') * 2);
+
+    @include media-breakpoint-up(sm) {
+      top: theme('spacing.slim-header');
+    }
+  }
 }
 
 .Draftail-MetaToolbar {
   position: absolute;
   inset-inline-end: 0;
   visibility: hidden;
+  background-color: transparent;
+
+  &:empty {
+    display: none;
+  }
 
   // Make sure the toolbar is always visible for devices that do not hover.
   @media (hover: hover) {
@@ -233,13 +263,6 @@ $draftail-editor-font-family: $font-sans;
   display: none;
 }
 
-.Draftail-ToolbarGroup--controls::before {
-  display: inline-block;
-  height: 1.875rem;
-  background-color: $color-white;
-  opacity: 0.2;
-}
-
 .Draftail-ToolbarButton {
   height: 1.875rem;
   min-width: 1.875rem;
@@ -260,9 +283,29 @@ $draftail-editor-font-family: $font-sans;
   }
 }
 
-.Draftail-Editor__wrapper .public-DraftEditor-content {
-  @include input-base();
-  @include show-focus-outline-inside();
+.Draftail-ToolbarButton--pin {
+  min-width: theme('spacing.6');
+  height: theme('spacing.6');
+  border: 1px solid theme('colors.primary.DEFAULT');
+
+  &:hover {
+    border-color: theme('colors.primary.DEFAULT');
+  }
+
+  .Draftail-Toolbar & {
+    border-color: $color-input-border;
+    background-color: theme('colors.grey.50');
+    border-top-width: 0;
+    border-inline-end-width: 0;
+
+    .Draftail-Editor:hover & {
+      border-color: $color-input-hover-border;
+    }
+  }
+
+  .icon {
+    transform: rotate(30deg);
+  }
 }
 
 .Draftail-block--blockquote {

+ 49 - 1
client/src/components/Draftail/index.js

@@ -38,6 +38,49 @@ const BR_ICON =
   'M.436 633.471l296.897-296.898v241.823h616.586V94.117h109.517v593.796H297.333v242.456z';
 const ADD_ICON = <Icon name="plus" />;
 
+const pinButton = {
+  floatingIcon: <Icon name="thumbtack" />,
+  stickyIcon: <Icon name="thumbtack-crossed" />,
+  floatingDescription: gettext('Pin toolbar'),
+  stickyDescription: gettext('Unpin toolbar'),
+};
+
+const getSavedToolbar = () => {
+  let saved = 'floating';
+  try {
+    saved = localStorage.getItem('wagtail:draftail-toolbar') || saved;
+  } catch {
+    // Use the default if localStorage isn’t available.
+  }
+  return saved;
+};
+
+/**
+ * Scroll to keep the field on the same spot when switching toolbars,
+ * and save the choice in localStorage.
+ */
+const onSetToolbar = (choice, callback) => {
+  const activeEditor = document.activeElement;
+  const before = activeEditor.getBoundingClientRect().top;
+  callback(choice);
+
+  // Delay scrolling until reflow has been fully computed.
+  requestAnimationFrame(() => {
+    const after = activeEditor.getBoundingClientRect().top;
+    const scrollArea = document.querySelector('#main');
+    scrollArea.scrollBy({
+      // Scroll by a positive amount if the editor moved down, negative if up.
+      top: after - before,
+      behavior: 'instant',
+    });
+  });
+  try {
+    localStorage.setItem('wagtail:draftail-toolbar', choice);
+  } catch {
+    // Skip saving the preference if localStorage isn’t available.
+  }
+};
+
 /**
  * Registry for client-side code of Draftail plugins.
  */
@@ -153,7 +196,12 @@ const initEditor = (selector, originalOptions, currentScript) => {
             comboPlaceholder={gettext('Search blocks')}
             noResultsText={gettext('No results')}
           />
-          <InlineToolbar {...props} />
+          <InlineToolbar
+            {...props}
+            pinButton={pinButton}
+            defaultToolbar={getSavedToolbar()}
+            onSetToolbar={onSetToolbar}
+          />
         </>
       ),
       bottomToolbar: MetaToolbar,

+ 2 - 3
client/src/entrypoints/admin/telepath/widgets.js

@@ -197,9 +197,8 @@ class DraftailInsertBlockCommand {
   }
 
   onSelect({ editorState }) {
-    // Reset the current block to unstyled and empty before splitting, so we remove the command prompt if used.
     const result = window.draftail.splitState(
-      window.draftail.DraftUtils.resetBlockWithType(editorState, 'unstyled'),
+      window.draftail.DraftUtils.removeCommandPalettePrompt(editorState),
     );
     if (result.stateAfter.getCurrentContent().hasText()) {
       // There is content after the insertion point, so need to split the existing block.
@@ -246,7 +245,7 @@ class DraftailSplitCommand {
 
   onSelect({ editorState }) {
     const result = window.draftail.splitState(
-      window.draftail.DraftUtils.resetBlockWithType(editorState, 'unstyled'),
+      window.draftail.DraftUtils.removeCommandPalettePrompt(editorState),
     );
     // Run the split after a timeout to circumvent potential race condition.
     setTimeout(() => {

+ 2 - 0
docs/releases/4.0.md

@@ -37,6 +37,8 @@ Following from Wagtail 3.0, this release contains significant UI changes that af
 
 Further updates to the page editor are expected in the next release. Those changes were implemented by Thibaud Colas. Development on this feature was sponsored by Google.
 
+(rich_text_improvements_4)=
+
 ### Rich text improvements
 
 As part of the page editor redesign project sponsored by Google, we have made a number of improvements to our rich text editor:

+ 13 - 0
docs/releases/4.2.md

@@ -27,6 +27,16 @@ Wagtail now provides a `fullpageurl` template tag (for both Django templates and
 
 This feature was developed by Jake Howard.
 
+### Rich text improvements
+
+Following feedback from Wagtail users on [rich text UI improvements in Wagtail 4.0](rich_text_improvements_4), we have further refined the behavior of rich text fields to cater for different scenarios:
+
+- Users can now choose between an “inline” floating toolbar, and a fixed toolbar at the top of the editor. Both toolbars display all formatting options.
+- The ‘/’ command palette and block picker in rich text fields now contain all formatting options except text styles.
+- The ‘/’ command palette and block picker are now always available no matter where the cursor is placed, to support inserting content at any point within text, transforming existing content, and splitting StreamField blocks in the middle of a paragraph when needed.
+
+Thank you to all who provided feedback, participants to our usability testing sessions, and to Nick Lee and Thibaud Colas for the implementation.
+
 ### Other features
 
  * Test assertion [`WagtailPageTestCase.assertCanCreate`](testing_reference) now supports the kwarg `publish=True` to check publish redirection (Harry Percival, Akua Dokua Asiedu)
@@ -87,6 +97,9 @@ This feature was developed by Jake Howard.
  * Resolve issue where workflow and other notification emails would not include the correct tab URL for account notification management (LB (Ben) Johnston)
  * Use consistent spacing above and below page headers (Thibaud Colas)
  * Use the correct icon sizes and spacing in slim header (Thibaud Colas)
+ * Use the correct color for placeholders in rich text fields (Thibaud Colas)
+ * Prevent obstructing the outline around rich text fields (Thibaud Colas)
+ * Page editor dropdowns now use indigo backgrounds like elsewhere in the admin interface (Thibaud Colas)
 
 ### Documentation
 

+ 53 - 10
package-lock.json

@@ -13,7 +13,7 @@
         "a11y-dialog": "^7.4.0",
         "axe-core": "^4.6.2",
         "draft-js": "^0.10.5",
-        "draftail": "^2.0.0-rc.2",
+        "draftail": "^2.0.0-rc.5",
         "draftjs-filters": "^3.0.1",
         "focus-trap-react": "^8.4.2",
         "immer": "^9.0.6",
@@ -16540,6 +16540,7 @@
     },
     "node_modules/compute-scroll-into-view": {
       "version": "1.0.17",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/concat-map": {
@@ -17962,6 +17963,7 @@
     },
     "node_modules/downshift": {
       "version": "6.1.7",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@babel/runtime": "^7.14.8",
@@ -18004,13 +18006,13 @@
       }
     },
     "node_modules/draftail": {
-      "version": "2.0.0-rc.2",
-      "resolved": "https://registry.npmjs.org/draftail/-/draftail-2.0.0-rc.2.tgz",
-      "integrity": "sha512-3KNMXv54k0yxAoOk8Ho9m/YRxJxArv7VwS/3X1yX0Xi2dUzvRzvYW5piGMSIX6vgYoWSN9p5bM+XtytciL93ig==",
+      "version": "2.0.0-rc.5",
+      "resolved": "https://registry.npmjs.org/draftail/-/draftail-2.0.0-rc.5.tgz",
+      "integrity": "sha512-t4o+483o7DY+7taNP6adgh2FAp4VBi0WxcteilPZdRZaotv3ePsLV5TPtfLiQtS4KGgGyP+RiGmPfPjJ/Ycbvg==",
       "dependencies": {
         "@tippyjs/react": "^4.2.6",
         "decorate-component-with-props": "^1.0.2",
-        "downshift": "^6.1.7",
+        "downshift": "^7.0.4",
         "draft-js-plugins-editor": "^2.1.1",
         "draftjs-conductor": "^3.0.0",
         "draftjs-filters": "^3.0.1"
@@ -18021,6 +18023,26 @@
         "react-dom": "^16.6.0"
       }
     },
+    "node_modules/draftail/node_modules/compute-scroll-into-view": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
+      "integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
+    },
+    "node_modules/draftail/node_modules/downshift": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.2.0.tgz",
+      "integrity": "sha512-dEn1Sshe7iTelUhmdbmiJhtIiwIBxBV8p15PuvEBh0qZcHXZnEt0geuCIIkCL4+ooaKRuLE0Wc+Fz9SwWuBIyg==",
+      "dependencies": {
+        "@babel/runtime": "^7.14.8",
+        "compute-scroll-into-view": "^2.0.4",
+        "prop-types": "^15.7.2",
+        "react-is": "^17.0.2",
+        "tslib": "^2.3.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.12.0"
+      }
+    },
     "node_modules/draftjs-conductor": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/draftjs-conductor/-/draftjs-conductor-3.0.0.tgz",
@@ -43078,7 +43100,8 @@
       }
     },
     "compute-scroll-into-view": {
-      "version": "1.0.17"
+      "version": "1.0.17",
+      "dev": true
     },
     "concat-map": {
       "version": "0.0.1",
@@ -44064,6 +44087,7 @@
     },
     "downshift": {
       "version": "6.1.7",
+      "dev": true,
       "requires": {
         "@babel/runtime": "^7.14.8",
         "compute-scroll-into-view": "^1.0.17",
@@ -44091,16 +44115,35 @@
       }
     },
     "draftail": {
-      "version": "2.0.0-rc.2",
-      "resolved": "https://registry.npmjs.org/draftail/-/draftail-2.0.0-rc.2.tgz",
-      "integrity": "sha512-3KNMXv54k0yxAoOk8Ho9m/YRxJxArv7VwS/3X1yX0Xi2dUzvRzvYW5piGMSIX6vgYoWSN9p5bM+XtytciL93ig==",
+      "version": "2.0.0-rc.5",
+      "resolved": "https://registry.npmjs.org/draftail/-/draftail-2.0.0-rc.5.tgz",
+      "integrity": "sha512-t4o+483o7DY+7taNP6adgh2FAp4VBi0WxcteilPZdRZaotv3ePsLV5TPtfLiQtS4KGgGyP+RiGmPfPjJ/Ycbvg==",
       "requires": {
         "@tippyjs/react": "^4.2.6",
         "decorate-component-with-props": "^1.0.2",
-        "downshift": "^6.1.7",
+        "downshift": "^7.0.4",
         "draft-js-plugins-editor": "^2.1.1",
         "draftjs-conductor": "^3.0.0",
         "draftjs-filters": "^3.0.1"
+      },
+      "dependencies": {
+        "compute-scroll-into-view": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
+          "integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
+        },
+        "downshift": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.2.0.tgz",
+          "integrity": "sha512-dEn1Sshe7iTelUhmdbmiJhtIiwIBxBV8p15PuvEBh0qZcHXZnEt0geuCIIkCL4+ooaKRuLE0Wc+Fz9SwWuBIyg==",
+          "requires": {
+            "@babel/runtime": "^7.14.8",
+            "compute-scroll-into-view": "^2.0.4",
+            "prop-types": "^15.7.2",
+            "react-is": "^17.0.2",
+            "tslib": "^2.3.0"
+          }
+        }
       }
     },
     "draftjs-conductor": {

+ 1 - 1
package.json

@@ -106,7 +106,7 @@
     "a11y-dialog": "^7.4.0",
     "axe-core": "^4.6.2",
     "draft-js": "^0.10.5",
-    "draftail": "^2.0.0-rc.2",
+    "draftail": "^2.0.0-rc.5",
     "draftjs-filters": "^3.0.1",
     "focus-trap-react": "^8.4.2",
     "immer": "^9.0.6",

+ 3 - 0
wagtail/admin/templates/wagtailadmin/icons/thumbtack-crossed.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="icon-thumbtack-crossed" viewBox="0 0 16 15">
+    <path d="M12.557 7.635H3.032a2.859 2.859 0 0 0-.486 1.578c0 .383.274.656.656.656h3.72v2.953l.655 1.313c.082.164.329.164.41 0l.657-1.313c0-.014.007-.027.014-.041a.097.097 0 0 0 .013-.041V9.869h3.719a.648.648 0 0 0 .656-.656c0-.583-.181-1.114-.489-1.578ZM10.34 2.869l.184 1.69H5.04l.185-1.69H4.077a.632.632 0 0 1-.656-.656V.9c0-.355.274-.656.656-.656h7.438c.355 0 .656.3.656.656v1.313c0 .382-.3.656-.656.656h-1.176ZM0 5.56h16v1.08H0V5.56Z"/>
+</svg>

+ 1 - 0
wagtail/admin/wagtail_hooks.py

@@ -1126,6 +1126,7 @@ def register_icons(icons):
         "tag.svg",
         "tasks.svg",
         "thumbtack.svg",
+        "thumbtack-crossed.svg",
         "tick-inverse.svg",
         "tick.svg",
         "time.svg",