浏览代码

Implement new reusable dialog component with a11y-dialog (#8541)

Co-authored-by: Thibaud Colas <thibaudcolas@gmail.com>
Steve Stein 2 年之前
父节点
当前提交
6343a9558c

+ 1 - 1
.circleci/config.yml

@@ -23,7 +23,7 @@ jobs:
       - run: pipenv run isort --check-only --diff .
       - run: pipenv run black --target-version py37 --check --diff .
       - run: git ls-files '*.html' | xargs pipenv run djhtml --check
-      - run: pipenv run curlylint --parse-only wagtail
+      - run: pipenv run curlylint --exclude '(dialog.html|end_dialog.html)' --parse-only wagtail
       - run: pipenv run doc8 docs
       - run: DATABASE_NAME=wagtail.db pipenv run python -u runtests.py
 

+ 1 - 1
Makefile

@@ -21,7 +21,7 @@ lint-server:
 	black --target-version py37 --check --diff .
 	flake8
 	isort --check-only --diff .
-	curlylint --parse-only wagtail
+	curlylint --exclude '(dialog.html|end_dialog.html)' --parse-only wagtail
 	git ls-files '*.html' | xargs djhtml --check
 
 lint-client:

+ 144 - 0
client/scss/components/_dialog.scss

@@ -0,0 +1,144 @@
+.w-dialog {
+  position: fixed;
+  display: flex;
+  inset-inline-start: 0;
+  inset-inline-end: 0;
+  bottom: 0;
+  top: 0;
+  z-index: theme('zIndex.dialog');
+  padding: theme('spacing.4');
+
+  &[aria-hidden='true'] {
+    display: none;
+  }
+
+  &__overlay {
+    position: fixed;
+    inset-inline-start: 0;
+    inset-inline-end: 0;
+    bottom: 0;
+    top: 0;
+    opacity: theme('opacity.40');
+    background: theme('colors.black.DEFAULT');
+  }
+
+  &__box {
+    width: 100%;
+    position: relative;
+    margin: auto;
+    overflow: hidden;
+    max-width: theme('maxWidth.2xl');
+    z-index: theme('zIndex.dialog');
+    background: theme('colors.white.DEFAULT');
+    box-shadow: theme('boxShadow.DEFAULT');
+    border-radius: theme('borderRadius.md');
+    animation: theme('animation.fade-in');
+
+    @include media-breakpoint-up(sm) {
+      width: 600px;
+    }
+  }
+
+  &__close-button {
+    position: absolute;
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    background: transparent;
+    padding: 0;
+    top: theme('spacing.2');
+    inset-inline-end: theme('spacing.2');
+    width: theme('spacing.12');
+    height: theme('spacing.12');
+  }
+
+  &__close-icon {
+    width: theme('spacing.4');
+    height: theme('spacing.4');
+    color: theme('colors.grey.600');
+  }
+
+  &__content {
+    // Using apply for scrollbars to avoid having to overwrite scrollbar with excess css
+    overflow: auto;
+    padding: theme('spacing.8');
+    max-height: calc(100vh - 180px);
+
+    @include media-breakpoint-up(sm) {
+      padding: theme('spacing.12');
+    }
+
+    @include media-breakpoint-up(md) {
+      padding-inline-start: theme('spacing.20');
+      padding-inline-end: theme('spacing.20');
+    }
+  }
+
+  &__icon {
+    position: absolute;
+    display: none;
+    width: theme('spacing.5');
+    height: theme('spacing.5');
+    color: theme('colors.primary.DEFAULT');
+    top: theme('spacing.[0.5]');
+    transform: translateY(theme('spacing.2'));
+    inset-inline-start: calc(0 - theme('spacing.10'));
+
+    @include media-breakpoint-up(md) {
+      display: block;
+    }
+  }
+
+  &__title {
+    position: relative;
+    margin-top: 0;
+    margin-bottom: theme('spacing.1');
+  }
+
+  &__subtitle {
+    margin-bottom: theme('spacing.4');
+  }
+
+  &__message {
+    display: flex;
+    align-items: center;
+    padding: theme('spacing.5');
+
+    &--info {
+      background: theme('colors.info.50');
+      color: theme('colors.info.100');
+    }
+
+    &--warning {
+      background: theme('colors.warning.50');
+      color: theme('colors.primary.DEFAULT');
+    }
+
+    &--critical {
+      background: theme('colors.critical.50');
+      color: theme('colors.critical.200');
+    }
+
+    &--success {
+      background: theme('colors.positive.50');
+      color: theme('colors.positive.100');
+    }
+  }
+
+  &__message-icon {
+    width: theme('spacing.5');
+    height: theme('spacing.5');
+    flex-shrink: 0;
+  }
+
+  &__message-header {
+    margin-inline-start: theme('spacing[2.5]');
+    padding-inline-end: theme('spacing.8');
+    color: theme('colors.grey.600');
+    font-size: theme('fontSize.14');
+  }
+
+  &__message-description {
+    margin-bottom: 0;
+  }
+}

+ 1 - 0
client/scss/core.scss

@@ -106,6 +106,7 @@ These are classes for components.
 // Legacy
 @import 'components/icons';
 @import 'components/tabs';
+@import 'components/dialog';
 @import 'components/dropdown';
 @import 'components/dropdown.legacy';
 @import 'components/help-block';

+ 2 - 2
client/src/components/Sidebar/Sidebar.scss

@@ -36,7 +36,7 @@
 
 .sidebar,
 .sidebar-loading {
-  @apply w-fixed w-flex w-flex-col w-h-full w-bg-primary w-z-[300] w-transition-sidebar;
+  @apply w-fixed w-flex w-flex-col w-h-full w-bg-primary w-z-sidebar w-transition-sidebar;
   width: $menu-width;
   inset-inline-start: 0;
 
@@ -86,9 +86,9 @@
 
 // This is a separate component as it needs to display in the header
 .sidebar-nav-toggle {
+  @apply w-z-sidebar-toggle;
   @include sidebar-toggle;
   display: none; // Nav toggle is for mobile only
-  z-index: 305;
 
   &--mobile {
     @apply w-bg-primary w-top-0 w-left-0 w-h-[50px] w-w-[50px] w-rounded-none hover:w-bg-primary-200;

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

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

+ 21 - 0
client/src/includes/dialog.js

@@ -0,0 +1,21 @@
+import A11yDialog from 'a11y-dialog';
+
+export const dialog = (
+  dialogs = document.querySelectorAll('[data-dialog]'),
+) => {
+  dialogs.forEach((template) => {
+    const html = document.documentElement;
+    const templateContent = template.content.firstElementChild.cloneNode(true);
+    document.body.appendChild(templateContent);
+    const dialogTemplate = new A11yDialog(templateContent);
+
+    // Prevent scrolling when dialog is open
+    dialogTemplate
+      .on('show', () => {
+        html.style.overflowY = 'hidden';
+      })
+      .on('hide', () => {
+        html.style.overflowY = '';
+      });
+  });
+};

+ 1 - 1
client/src/tokens/colors.js

@@ -159,7 +159,7 @@ const colors = {
   },
   warning: {
     100: {
-      hex: '#FFBF00',
+      hex: '#FAA500',
       bgUtility: 'w-bg-warning-100',
       textUtility: 'w-text-warning-100',
       usage: 'Background and icons for potentially dangerous states',

+ 1 - 1
client/src/tokens/objectStyles.js

@@ -15,7 +15,7 @@ const borderWidth = {
 
 // If adding new values, use T-shirt sizing naming.
 const boxShadow = {
-  DEFAULT: '5px 5px 20px rgb(0 0 0 / 0.1)',
+  DEFAULT: '5px 5px 20px rgba(0, 0, 0, 0.05)',
 };
 
 module.exports = {

+ 13 - 1
client/tailwind.config.js

@@ -84,7 +84,19 @@ module.exports = {
           'inset-inline-start, padding-inline-start, width, transform, margin-top, min-height',
       },
       zIndex: {
-        header: '100',
+        'header': '100',
+        'sidebar': '110',
+        'sidebar-toggle': '120',
+        'dialog': '130',
+      },
+      keyframes: {
+        'fade-in': {
+          '0%': { opacity: 0 },
+          '100%': { opacity: 1 },
+        },
+      },
+      animation: {
+        'fade-in': 'fade-in 150ms both',
       },
     },
   },

+ 27 - 0
package-lock.json

@@ -9,6 +9,7 @@
       "version": "1.0.0",
       "dependencies": {
         "@tippyjs/react": "^4.2.6",
+        "a11y-dialog": "^7.4.0",
         "draft-js": "^0.10.5",
         "draftail": "^1.4.1",
         "draftjs-filters": "^2.5.0",
@@ -13986,6 +13987,14 @@
       "dev": true,
       "license": "Apache-2.0"
     },
+    "node_modules/a11y-dialog": {
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-7.4.0.tgz",
+      "integrity": "sha512-NrLXedddjbIxvapn0ac488DkVuWZjCNxLxHxXPUxjNF7crdHJ4dquPwJ89dV21IPQsXtA5YHNn++Mxouu+ciuQ==",
+      "dependencies": {
+        "focusable-selectors": "^0.3.1"
+      }
+    },
     "node_modules/abab": {
       "version": "2.0.5",
       "dev": true,
@@ -19797,6 +19806,11 @@
         "react-dom": ">=16.0.0"
       }
     },
+    "node_modules/focusable-selectors": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/focusable-selectors/-/focusable-selectors-0.3.1.tgz",
+      "integrity": "sha512-5JLtr0e1YJIfmnVlpLiG+av07dd0Xkf/KfswsXcei5KmLfdwOysTQsjF058ynXniujb1fvev7nql1x+CkC5ikw=="
+    },
     "node_modules/follow-redirects": {
       "version": "1.14.9",
       "dev": true,
@@ -41137,6 +41151,14 @@
       "version": "4.2.2",
       "dev": true
     },
+    "a11y-dialog": {
+      "version": "7.4.0",
+      "resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-7.4.0.tgz",
+      "integrity": "sha512-NrLXedddjbIxvapn0ac488DkVuWZjCNxLxHxXPUxjNF7crdHJ4dquPwJ89dV21IPQsXtA5YHNn++Mxouu+ciuQ==",
+      "requires": {
+        "focusable-selectors": "^0.3.1"
+      }
+    },
     "abab": {
       "version": "2.0.5",
       "dev": true
@@ -45111,6 +45133,11 @@
         "focus-trap": "^6.7.2"
       }
     },
+    "focusable-selectors": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/focusable-selectors/-/focusable-selectors-0.3.1.tgz",
+      "integrity": "sha512-5JLtr0e1YJIfmnVlpLiG+av07dd0Xkf/KfswsXcei5KmLfdwOysTQsjF058ynXniujb1fvev7nql1x+CkC5ikw=="
+    },
     "follow-redirects": {
       "version": "1.14.9",
       "dev": true

+ 1 - 0
package.json

@@ -98,6 +98,7 @@
   },
   "dependencies": {
     "@tippyjs/react": "^4.2.6",
+    "a11y-dialog": "^7.4.0",
     "draft-js": "^0.10.5",
     "draftail": "^1.4.1",
     "draftjs-filters": "^2.5.0",

+ 2 - 2
wagtail/admin/templates/wagtailadmin/icons/warning.svg

@@ -1,3 +1,3 @@
-<svg id="icon-warning" viewBox="0 0 16 16">
-    <path d="M14.674 13.246c0.11 0.165 0.11 0.33 0 0.494-0.082 0.137-0.22 0.22-0.412 0.22 0 0-12.552 0-12.552 0-0.165 0-0.302-0.082-0.385-0.22-0.11-0.165-0.11-0.33-0.027-0.494 0 0 6.262-10.986 6.262-10.986 0.082-0.165 0.22-0.247 0.439-0.247 0.192 0 0.33 0.082 0.412 0.247 0 0 6.262 10.986 6.262 10.986zM8.769 12.559c0 0 0-1.401 0-1.401s-1.538 0-1.538 0c0 0 0 1.401 0 1.401s1.538 0 1.538 0zM8.769 10.115c0 0 0-4.23 0-4.23s-1.538 0-1.538 0c0 0 0 4.23 0 4.23s1.538 0 1.538 0z"></path>
+<svg id="icon-warning"  viewBox="0 0 512 512">
+    <path d="M506.3 417 293 53c-16.33-28-57.54-28-73.98 0L5.82 417c-16.41 27.9 4.029 63 36.92 63h426.6c32.76 0 53.26-35 36.96-63zM232 168c0-13.25 10.75-24 24-24s24 10.8 24 24v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zm24 248c-17.36 0-31.44-14.08-31.44-31.44s14.07-31.44 31.44-31.44 31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/>
 </svg>

+ 12 - 0
wagtail/admin/templates/wagtailadmin/shared/dialog/dialog-toggle.html

@@ -0,0 +1,12 @@
+{% load wagtailadmin_tags %}
+{% comment %}
+    Variables this template accepts:
+
+    dialog_id - The ID of the dialog you are toggling open
+    class_name - CSS classes for styling the button
+    text - The text of the button
+
+{% endcomment %}
+<button type="button" class="{{ class_name }}" data-a11y-dialog-show="{{ dialog_id }}">
+    {{ text }}
+</button>

+ 39 - 0
wagtail/admin/templates/wagtailadmin/shared/dialog/dialog.html

@@ -0,0 +1,39 @@
+{% load wagtailadmin_tags i18n %}
+<template data-dialog>
+    <div
+        id="{{ id }}"
+        aria-labelledby="title-{{ id }}"
+        aria-hidden="true"
+        class="w-dialog"
+    >
+        <div data-a11y-dialog-hide class="w-dialog__overlay"></div>
+        <div class="w-dialog__box">
+            <button type="button" data-a11y-dialog-hide aria-label="{% trans 'Close dialog' %}" class="w-dialog__close-button">
+                {% icon name='cross' class_name="w-dialog__close-icon" %}
+            </button>
+
+            {% if message_heading and message_icon_name %}
+                <div class="w-dialog__message w-dialog__message--{{ message_status }}">
+                    {% icon name=message_icon_name class_name="w-dialog__message-icon" %}
+                    <div class="w-dialog__message-header">
+                        <strong class="w-dialog__message-heading">{{ message_heading }}</strong>
+                        {% if message_description %}<p class="w-dialog__message-description ">{{ message_description }}</p>{% endif %}
+                    </div>
+                </div>
+            {% endif %}
+
+            <div class="w-dialog__content">
+                <h2 class="w-dialog__title w-h1" id="title-{{ id }}">
+                    {% if icon_name %}{% icon name=icon_name class_name="w-dialog__icon" %}{% endif %}
+                    {{ title }}
+                </h2>
+
+                {% if subtitle %}
+                    <p class="w-dialog__subtitle w-help-text">{{ subtitle }}</p>
+                {% endif %}
+
+
+                {% comment %}
+    This markup is intentionally left without closing div tags so that the contents can be populated with child elements between dialog and enddialog
+    For the end tags please see end-dialog.html
+                {% endcomment %}

+ 8 - 0
wagtail/admin/templates/wagtailadmin/shared/dialog/end-dialog.html

@@ -0,0 +1,8 @@
+</div>
+</div>
+</div>
+</template>
+
+{% comment %}
+    This markup is used to close the end tags for dialog.html so that content can be nested between both tags like {% dialog %}{% enddialog %}
+{% endcomment %}

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

@@ -540,7 +540,6 @@ def page_header_buttons(context, page, page_perms):
 
 @register.inclusion_tag("wagtailadmin/pages/listing/_buttons.html", takes_context=True)
 def bulk_action_choices(context, app_label, model_name):
-
     bulk_actions_list = list(
         bulk_action_registry.get_bulk_actions_for_model(app_label, model_name)
     )
@@ -893,3 +892,74 @@ def component(context, obj, fallback_render_method=False):
         raise ValueError("Cannot render %r as a component" % (obj,))
 
     return obj.render_html(context)
+
+
+@register.inclusion_tag("wagtailadmin/shared/dialog/dialog.html")
+def dialog(
+    id,
+    title,
+    icon_name=None,
+    subtitle=None,
+    message_status=None,
+    message_heading=None,
+    message_description=None,
+):
+    """
+    Dialog tag - to be used with its corresponding {% enddialog %} tag with dialog content markup nested between
+    """
+    if not title:
+        raise ValueError("You must supply a title")
+    if not id:
+        raise ValueError("You must supply an id")
+
+    # Used for determining which icon the message will use
+    message_status_type = {
+        "info": {
+            "message_icon_name": "info-circle",
+        },
+        "warning": {
+            "message_icon_name": "warning",
+        },
+        "critical": {
+            "message_icon_name": "warning",
+        },
+        "success": {
+            "message_icon_name": "circle-check",
+        },
+    }
+
+    context = {
+        "id": id,
+        "title": title,
+        "icon_name": icon_name,
+        "subtitle": subtitle,
+        "message_heading": message_heading,
+        "message_description": message_description,
+        "message_status": message_status,
+    }
+
+    # If there is a message status then add the context for that message type
+    if message_status:
+        context.update(**message_status_type[message_status])
+
+    return context
+
+
+# Closing tag for dialog tag {% enddialog %}
+@register.inclusion_tag("wagtailadmin/shared/dialog/end-dialog.html")
+def enddialog():
+    return
+
+
+# Button used to open dialogs
+@register.inclusion_tag("wagtailadmin/shared/dialog/dialog-toggle.html")
+def dialog_toggle(dialog_id, class_name="", text=None):
+    if not dialog_id:
+        raise ValueError("You must supply the dialog ID")
+
+    return {
+        "class_name": class_name,
+        "text": text,
+        # dialog_id must match the ID of the dialog you are toggling
+        "dialog_id": dialog_id,
+    }

+ 49 - 6
wagtail/contrib/styleguide/templates/wagtailstyleguide/base.html

@@ -28,6 +28,7 @@
                 <li><a href="#pagination">Pagination</a></li>
                 <li><a href="#buttons">Buttons</a></li>
                 <li><a href="#dropdowns">Dropdown buttons</a></li>
+                <li><a href="#dialog">Dialogs</a></li>
                 <li><a href="#header">Header</a></li>
                 <li><a href="#forms">Forms</a></li>
                 <li><a href="#editor">Page editor</a></li>
@@ -469,10 +470,10 @@
 
             <h4>Arbitrarily bigger</h4>
             <style>
-                #button-arbitrarily-bigger{
-                    font-size:1.5em;
-                    padding:1.1em 2.4em;
-                    height:3.5em;
+                #button-arbitrarily-bigger {
+                    font-size: 1.5em;
+                    padding: 1.1em 2.4em;
+                    height: 3.5em;
                 }
             </style>
             <button class="button button-longrunning" id="button-arbitrarily-bigger">{% icon name="spinner" %}Click me</button>
@@ -494,7 +495,7 @@
                 <button class="button">button element</button>
                 <button class="button">button element</button>
             </div>
-            <br />
+            <br/>
             <div class="button-group">
                 <button class="button button--icon text-replace yes">{% icon name="tick" %}A proper button</button>
                 <a href="#" class="button button--icon text-replace white">{% icon name="cog" %}A link button</a>
@@ -625,7 +626,7 @@
                 </div>
             </div>
 
-            <p>These can also have an inverted  theme:</p>
+            <p>These can also have an inverted theme:</p>
             <header class="header">
                 <div class="c-dropdown  t-inverted" data-dropdown="">
                     <a class="c-dropdown__button  u-btn-current">
@@ -652,6 +653,48 @@
 
         </section>
 
+        <section id="dialog">
+            <h2>Dialogs</h2>
+
+            <div class="w-flex w-gap-4">
+                <div>
+                    {% dialog_toggle class_name='button button-primary' dialog_id='dialog-1' text='Simple dialog' %}
+
+                    {% dialog icon_name="doc-full-inverse" id="dialog-1" title="Simple dialog" subtitle="This is as simple as it gets 😀" %}
+                        <p class="w-base-text">This is an example of a simple dialog with an icon_name, title and subtitle passed to the dialog tag</p>
+                    {% enddialog %}
+                </div>
+                <div>
+                    {% dialog_toggle class_name='button button-primary' dialog_id='dialog-2' text='Dialog with info' %}
+
+                    {% dialog icon_name="globe" title="Dialog with info" id="dialog-2" subtitle="This is a testing subtitle" message_status="info" message_heading="Here is some info on the thing" message_description="This is a subtext for the message" %}
+                        <p class="w-base-text">This dialog message was generated by passing message_status=info as well as message_heading and message_description to the dialog template tag</p>
+                    {% enddialog %}
+                </div>
+                <div>
+                    {% dialog_toggle class_name='button button-primary' dialog_id='dialog-3' text='Dialog with success' %}
+
+                    {% dialog icon_name="globe" title="Dialog with success" id="dialog-3" subtitle="This is a testing subtitle" message_status="success" message_heading="Success! You've done the thing"  %}
+                        <p class="w-base-text">This dialog message was generated by passing message_status=success as well as message_heading to the dialog template tag</p>
+                    {% enddialog %}
+                </div>
+                <div>
+                    {% dialog_toggle class_name='button button-primary' dialog_id='dialog-4' text='Dialog with warning' %}
+
+                    {% dialog icon_name="globe" title="Dialog with warning" id="dialog-4" subtitle="This is a testing subtitle" message_status="warning" message_heading="There was an issue with the thing" message_description="This is a subtext for the message" %}
+                        <p class="w-base-text">This dialog message was generated by passing message_status=warning as well as message_heading and message_description to the dialog template tag</p>
+                    {% enddialog %}
+                </div>
+                <div>
+                    {% dialog_toggle class_name='button button-primary' dialog_id='dialog-5' text='Dialog with error' %}
+
+                    {% dialog icon_name="globe" title="Dialog with critical error" id="dialog-5" subtitle="This is a testing subtitle" message_status="critical" message_heading="There was an issue with the thing" message_description="This is a subtext for the message" %}
+                        <p class="w-base-text">This dialog message was generated by passing message_status=critical as well as message_heading and message_description to the dialog template tag</p>
+                    {% enddialog %}
+                </div>
+            </div>
+        </section>
+
         <section id="header">
             <h2>Header</h2>