diff --git a/composer.json b/composer.json
index 5d2e44182ede476eea90c023b27a0bf651e254a6..2c8c75f307464b4a2db4e24b73138aeff2fd90b0 100644
--- a/composer.json
+++ b/composer.json
@@ -180,7 +180,7 @@
         "drupal/views_fieldsets": "3.3",
         "drupal/views_infinite_scroll": "1.5",
         "drupal/views_slideshow": "4.4",
-        "drupal/webform": "5.0-rc12",
+        "drupal/webform": "5.2",
         "drupal/webform_views": "5.0-alpha2",
         "drush-ops/behat-drush-endpoint": "^0.0.5",
         "drush/drush": "^9",
diff --git a/composer.lock b/composer.lock
index 4716f95d4eeb62c94e397da1db0aeda612e7382e..0daea23f3b82839f9ea3226064fcabff8704a901 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "7604230aa9adf47dda5866685c99ae24",
+    "content-hash": "05189e0457ebaecf7dad6250a18bce1b",
     "packages": [
         {
             "name": "alchemy/zippy",
@@ -8026,26 +8026,35 @@
         },
         {
             "name": "drupal/webform",
-            "version": "5.0.0-rc12",
+            "version": "5.2.0",
             "source": {
                 "type": "git",
                 "url": "https://git.drupalcode.org/project/webform.git",
-                "reference": "8.x-5.0-rc12"
+                "reference": "8.x-5.2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://ftp.drupal.org/files/projects/webform-8.x-5.0-rc12.zip",
-                "reference": "8.x-5.0-rc12",
-                "shasum": "d677eb59bf2084267476fa482b5fbccb0dc8afb9"
+                "url": "https://ftp.drupal.org/files/projects/webform-8.x-5.2.zip",
+                "reference": "8.x-5.2",
+                "shasum": "44e67c377e156f7f8d6f26bba43240dfa1885637"
             },
             "require": {
                 "drupal/core": "*"
             },
             "require-dev": {
+                "drupal/address": "~1.4",
+                "drupal/captcha": "~1.0",
+                "drupal/chosen": "~2.6",
                 "drupal/devel": "*",
-                "drupal/token": "*",
+                "drupal/jsonapi": "~2.0",
+                "drupal/mailsystem": "~4.0",
+                "drupal/select2": "~1.1",
+                "drupal/telephone_validation": "^2.2",
+                "drupal/token": "~1.3",
+                "drupal/webform_access": "*",
                 "drupal/webform_node": "*",
-                "drupal/webform_scheduled_email": "*"
+                "drupal/webform_scheduled_email": "*",
+                "drupal/webform_ui": "*"
             },
             "type": "drupal-module",
             "extra": {
@@ -8053,11 +8062,11 @@
                     "dev-5.x": "5.x-dev"
                 },
                 "drupal": {
-                    "version": "8.x-5.0-rc12",
-                    "datestamp": "1528137480",
+                    "version": "8.x-5.2",
+                    "datestamp": "1563460085",
                     "security-coverage": {
-                        "status": "not-covered",
-                        "message": "RC releases are not covered by Drupal security advisories."
+                        "status": "covered",
+                        "message": "Covered by Drupal's security advisory policy"
                     }
                 },
                 "drush": {
@@ -8118,6 +8127,7 @@
             "homepage": "https://drupal.org/project/webform",
             "support": {
                 "source": "http://cgit.drupalcode.org/webform",
+                "error": "Invalid dependency: \"telephone_validation/telephone_validation\" is an unknown drupal 8 package name",
                 "issues": "https://www.drupal.org/project/issues/webform?version=8.x",
                 "docs": "https://www.drupal.org/docs/8/modules/webform",
                 "forum": "https://drupal.stackexchange.com/questions/tagged/webform"
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index 16c9d039d41b25e5dbfa7324e885d5aac0848c8c..81688fa97cd6f4ddfc7d9e009c627f9be1c4ae67 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -6032,18 +6032,18 @@
     },
     {
         "name": "drupal/metatag",
-        "version": "1.8.0",
-        "version_normalized": "1.8.0.0",
+        "version": "1.9.0",
+        "version_normalized": "1.9.0.0",
         "source": {
             "type": "git",
             "url": "https://git.drupalcode.org/project/metatag.git",
-            "reference": "8.x-1.8"
+            "reference": "8.x-1.9"
         },
         "dist": {
             "type": "zip",
-            "url": "https://ftp.drupal.org/files/projects/metatag-8.x-1.8.zip",
-            "reference": "8.x-1.8",
-            "shasum": "fb5d31aa08c8c2e175f096f9917e9741db152ea8"
+            "url": "https://ftp.drupal.org/files/projects/metatag-8.x-1.9.zip",
+            "reference": "8.x-1.9",
+            "shasum": "230960752c5afa17337fb69bae853bccb1a26ecd"
         },
         "require": {
             "drupal/core": "*",
@@ -6065,8 +6065,8 @@
                 "dev-1.x": "1.x-dev"
             },
             "drupal": {
-                "version": "8.x-1.8",
-                "datestamp": "1550692511",
+                "version": "8.x-1.9",
+                "datestamp": "1563995941",
                 "security-coverage": {
                     "status": "covered",
                     "message": "Covered by Drupal's security advisory policy"
@@ -8280,27 +8280,36 @@
     },
     {
         "name": "drupal/webform",
-        "version": "5.0.0-rc12",
-        "version_normalized": "5.0.0.0-RC12",
+        "version": "5.2.0",
+        "version_normalized": "5.2.0.0",
         "source": {
             "type": "git",
             "url": "https://git.drupalcode.org/project/webform.git",
-            "reference": "8.x-5.0-rc12"
+            "reference": "8.x-5.2"
         },
         "dist": {
             "type": "zip",
-            "url": "https://ftp.drupal.org/files/projects/webform-8.x-5.0-rc12.zip",
-            "reference": "8.x-5.0-rc12",
-            "shasum": "d677eb59bf2084267476fa482b5fbccb0dc8afb9"
+            "url": "https://ftp.drupal.org/files/projects/webform-8.x-5.2.zip",
+            "reference": "8.x-5.2",
+            "shasum": "44e67c377e156f7f8d6f26bba43240dfa1885637"
         },
         "require": {
             "drupal/core": "*"
         },
         "require-dev": {
+            "drupal/address": "~1.4",
+            "drupal/captcha": "~1.0",
+            "drupal/chosen": "~2.6",
             "drupal/devel": "*",
-            "drupal/token": "*",
+            "drupal/jsonapi": "~2.0",
+            "drupal/mailsystem": "~4.0",
+            "drupal/select2": "~1.1",
+            "drupal/telephone_validation": "^2.2",
+            "drupal/token": "~1.3",
+            "drupal/webform_access": "*",
             "drupal/webform_node": "*",
-            "drupal/webform_scheduled_email": "*"
+            "drupal/webform_scheduled_email": "*",
+            "drupal/webform_ui": "*"
         },
         "type": "drupal-module",
         "extra": {
@@ -8308,11 +8317,11 @@
                 "dev-5.x": "5.x-dev"
             },
             "drupal": {
-                "version": "8.x-5.0-rc12",
-                "datestamp": "1528137480",
+                "version": "8.x-5.2",
+                "datestamp": "1563460085",
                 "security-coverage": {
-                    "status": "not-covered",
-                    "message": "RC releases are not covered by Drupal security advisories."
+                    "status": "covered",
+                    "message": "Covered by Drupal's security advisory policy"
                 }
             },
             "drush": {
@@ -8374,6 +8383,7 @@
         "homepage": "https://drupal.org/project/webform",
         "support": {
             "source": "http://cgit.drupalcode.org/webform",
+            "error": "Invalid dependency: \"telephone_validation/telephone_validation\" is an unknown drupal 8 package name",
             "issues": "https://www.drupal.org/project/issues/webform?version=8.x",
             "docs": "https://www.drupal.org/docs/8/modules/webform",
             "forum": "https://drupal.stackexchange.com/questions/tagged/webform"
diff --git a/web/modules/webform/.eslintrc.json b/web/modules/webform/.eslintrc.json
new file mode 100644
index 0000000000000000000000000000000000000000..f9dbd5b66037683faf0a0fbb1fb4110293005e3f
--- /dev/null
+++ b/web/modules/webform/.eslintrc.json
@@ -0,0 +1,94 @@
+{
+  "extends": "eslint:recommended",
+  "root": true,
+  "env": {
+    "browser": true
+  },
+  "globals": {
+    "Drupal": true,
+    "drupalSettings": true,
+    "drupalTranslations": true,
+    "domready": true,
+    "jQuery": true,
+    "_": true,
+    "matchMedia": true,
+    "Backbone": true,
+    "Modernizr": true,
+    "CKEDITOR": true
+  },
+  "rules": {
+    "array-bracket-spacing": ["error", "never"],
+    "block-scoped-var": "error",
+    "brace-style": ["error", "stroustrup", {"allowSingleLine": true}],
+    "comma-dangle": ["error", "never"],
+    "comma-spacing": "error",
+    "comma-style": ["error", "last"],
+    "computed-property-spacing": ["error", "never"],
+    "curly": ["error", "all"],
+    "eol-last": "error",
+    "eqeqeq": ["error", "smart"],
+    "guard-for-in": "error",
+    "indent": ["error", 2, {"SwitchCase": 1}],
+    "key-spacing": ["error", {"beforeColon": false, "afterColon": true}],
+    "keyword-spacing": ["error", {"before": true, "after": true}],
+    "linebreak-style": ["error", "unix"],
+    "lines-around-comment": ["error", {"beforeBlockComment": true, "afterBlockComment": false}],
+    "new-parens": "error",
+    "no-array-constructor": "error",
+    "no-caller": "error",
+    "no-catch-shadow": "error",
+    "no-eval": "error",
+    "no-extend-native": "error",
+    "no-extra-bind": "error",
+    "no-extra-parens": ["error", "functions"],
+    "no-implied-eval": "error",
+    "no-iterator": "error",
+    "no-label-var": "error",
+    "no-labels": "error",
+    "no-lone-blocks": "error",
+    "no-loop-func": "error",
+    "no-multi-spaces": "error",
+    "no-multi-str": "error",
+    "no-native-reassign": "error",
+    "no-nested-ternary": "error",
+    "no-new-func": "error",
+    "no-new-object": "error",
+    "no-new-wrappers": "error",
+    "no-octal-escape": "error",
+    "no-process-exit": "error",
+    "no-proto": "error",
+    "no-return-assign": "error",
+    "no-script-url": "error",
+    "no-sequences": "error",
+    "no-shadow-restricted-names": "error",
+    "no-spaced-func": "error",
+    "no-trailing-spaces": "error",
+    "no-undef-init": "error",
+    "no-undefined": "error",
+    "no-unused-expressions": "error",
+    "no-unused-vars": ["error", {"vars": "all", "args": "none"}],
+    "no-with": "error",
+    "object-curly-spacing": ["error", "never"],
+    "one-var": ["error", "never"],
+    "quote-props": ["error", "consistent-as-needed"],
+    "quotes": ["error", "single", "avoid-escape"],
+    "semi": ["error", "always"],
+    "semi-spacing": ["error", {"before": false, "after": true}],
+    "space-before-blocks": ["error", "always"],
+    "space-before-function-paren": ["error", {"anonymous": "always", "named": "never"}],
+    "space-in-parens": ["error", "never"],
+    "space-infix-ops": "error",
+    "space-unary-ops": ["error", { "words": true, "nonwords": false }],
+    "spaced-comment": ["error", "always"],
+    "strict": ["error", "function"],
+    "yoda": ["error", "never"],
+    "max-nested-callbacks": ["warn", 3],
+    "valid-jsdoc": ["warn", {
+      "prefer": {
+        "returns": "return",
+        "property": "prop"
+      },
+      "requireReturn": false
+    }]
+  }
+}
diff --git a/web/modules/webform/.gitignore b/web/modules/webform/.gitignore
index 852c70afd649177fa13e7638ccb88e855e50d513..15e10416dd22941f574e3bc5d9d5493fe33ccc66 100644
--- a/web/modules/webform/.gitignore
+++ b/web/modules/webform/.gitignore
@@ -1,8 +1,10 @@
 *.patch
 interdiff-*.txt
 
-# Ignore the HTML directory used for generate Webform documentation.
-html
+# Ignore the HTML and reports directory used for generate Webform documentation.
+/html
+/reports/accessiblity/html
+/reports/accessiblity/pdf
 
 # Ignore all *.features.yml files.
 *.features.yml
diff --git a/web/modules/webform/ISSUE_TEMPLATE.html b/web/modules/webform/ISSUE_TEMPLATE.html
index b31b6d7e3d55f37a9f5f6de94605683205668f9a..dc0aee293d80d7be6aab316d70626823bdb4a382 100644
--- a/web/modules/webform/ISSUE_TEMPLATE.html
+++ b/web/modules/webform/ISSUE_TEMPLATE.html
@@ -30,4 +30,4 @@ <h3 id="summary-data-model-changes">Data model changes</h3>
 (OPTIONAL: Database or configuration data changes that would make stored data on an existing site incompatible with the site's updated codebase, including changes to hook_schema(), configuration schema or keys, or the expected format of stored data, etc.)
 
 <h3 id="summary-additional-information">Additional information</h3>
-(OPTIONAL: Include any additional information about your specific environment that could be helpful, including Drupal version, Browser version, etc...)
+(OPTIONAL: Include any additional information about your specific environment that could be helpful, including Drupal version, Browser version, etc…)
diff --git a/web/modules/webform/README.md b/web/modules/webform/README.md
index 2ce4711e4e31d23b58cd6d113d7d947e69fc6eb2..46e94cbe9870b136b999afad27b95f1ac5404ac9 100644
--- a/web/modules/webform/README.md
+++ b/web/modules/webform/README.md
@@ -58,18 +58,17 @@ The primary use case for this module is to:
 7. (optional) Install add-on contrib modules](/admin/structure/webform/addons).
 
 
-### Releases
-
-Even though the Webform module is still under active development with
-regular [beta releases](https://www.drupal.org/documentation/version-info/alpha-beta-rc),
-all existing configuration and submission data will be maintained and updated 
-between releases.  **APIs can and will be changing** while this module moves 
-from beta releases to a final release candidate. 
-
-Simply put, if you install and use the Webform module out of the box AS-IS, 
-you _should_ be okay.  Once you start extending webforms with plugins, altering 
-hooks, and overriding templates, you will need to read each release's 
-notes and assume that _things will be changing_.
+### Upgrading from pre-release versions
+
+All existing configuration and submission data was maintained and updated 
+through the beta and rc release cycles. 
+**APIs have changed** during these release cycles. 
+
+Simply put, if you installed and used the Webform module out of the box AS-IS, 
+and now you want to upgrade to a full release, then 
+you _should_ be okay. If you extended webforms with plugins, altered 
+hooks, and overrode templates, you will need to read each release's 
+notes and assume that _things have changed_.
 
 
 ### Project Status
diff --git a/web/modules/webform/composer.json b/web/modules/webform/composer.json
index 1114953a28d67389d70f2b52e29df61eedec4dd6..7be12f1613b555b96c8f7c4a84524cf762cee989 100644
--- a/web/modules/webform/composer.json
+++ b/web/modules/webform/composer.json
@@ -29,5 +29,15 @@
         "drush.services.yml": "^9"
       }
     }
+  },
+  "require-dev": {
+    "drupal/address": "~1.4",
+    "drupal/captcha": "~1.0",
+    "drupal/chosen": "~2.6",
+    "drupal/jsonapi": "~2.0",
+    "drupal/mailsystem": "~4.0",
+    "drupal/select2": "~1.1",
+    "drupal/telephone_validation": "^2.2",
+    "drupal/token": "~1.3"
   }
 }
diff --git a/web/modules/webform/composer.libraries.json b/web/modules/webform/composer.libraries.json
new file mode 100644
index 0000000000000000000000000000000000000000..31bda67b00ed3c88e2331a3faf888d887177210a
--- /dev/null
+++ b/web/modules/webform/composer.libraries.json
@@ -0,0 +1,429 @@
+{
+    "name": "drupal/webform",
+    "description": "Enables the creation of webforms and questionnaires.",
+    "type": "drupal-module",
+    "license": "GPL-2.0+",
+    "minimum-stability": "dev",
+    "homepage": "https://drupal.org/project/webform",
+    "authors": [
+        {
+            "name": "Jacob Rockowitz (jrockowitz)",
+            "homepage": "https://www.drupal.org/u/jrockowitz",
+            "role": "Maintainer"
+        },
+        {
+            "name": "Alexander Trotsenko (bucefal91)",
+            "homepage": "https://www.drupal.org/u/bucefal91",
+            "role": "Co-maintainer"
+        }
+    ],
+    "support": {
+        "issues": "https://www.drupal.org/project/issues/webform?version=8.x",
+        "source": "http://cgit.drupalcode.org/webform",
+        "docs": "https://www.drupal.org/docs/8/modules/webform",
+        "forum": "https://drupal.stackexchange.com/questions/tagged/webform"
+    },
+    "repositories": {
+        "algolia.places": {
+            "type": "package",
+            "package": {
+                "name": "algolia/places",
+                "version": "1.16.1",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "algolia.places"
+                },
+                "dist": {
+                    "url": "https://registry.npmjs.org/places.js/-/places.js-1.16.1.tgz",
+                    "type": "tar"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "ckeditor.autogrow": {
+            "type": "package",
+            "package": {
+                "name": "ckeditor/autogrow",
+                "version": "4.10.1",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "ckeditor.autogrow"
+                },
+                "dist": {
+                    "url": "https://download.ckeditor.com/autogrow/releases/autogrow_4.10.1.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "ckeditor.codemirror": {
+            "type": "package",
+            "package": {
+                "name": "ckeditor/codemirror",
+                "version": "v1.17.7",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "ckeditor.codemirror"
+                },
+                "dist": {
+                    "url": "https://github.com/w8tcha/CKEditor-CodeMirror-Plugin/releases/download/v1.17.7/CKEditor-CodeMirror-Plugin.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "ckeditor.fakeobjects": {
+            "type": "package",
+            "package": {
+                "name": "ckeditor/fakeobjects",
+                "version": "4.10.1",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "ckeditor.fakeobjects"
+                },
+                "dist": {
+                    "url": "https://download.ckeditor.com/fakeobjects/releases/fakeobjects_4.10.1.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "ckeditor.image": {
+            "type": "package",
+            "package": {
+                "name": "ckeditor/image",
+                "version": "4.10.1",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "ckeditor.image"
+                },
+                "dist": {
+                    "url": "https://download.ckeditor.com/image/releases/image_4.10.1.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "ckeditor.link": {
+            "type": "package",
+            "package": {
+                "name": "ckeditor/link",
+                "version": "4.10.1",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "ckeditor.link"
+                },
+                "dist": {
+                    "url": "https://download.ckeditor.com/link/releases/link_4.10.1.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "codemirror": {
+            "type": "package",
+            "package": {
+                "name": "codemirror/codemirror",
+                "version": "5.44.0",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "codemirror"
+                },
+                "dist": {
+                    "url": "https://github.com/components/codemirror/archive/5.44.0.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "jquery.chosen": {
+            "type": "package",
+            "package": {
+                "name": "jquery/chosen",
+                "version": "1.8.7",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "jquery.chosen"
+                },
+                "dist": {
+                    "url": "https://github.com/harvesthq/chosen/releases/download/v1.8.7/chosen_v1.8.7.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "jquery.geocomplete": {
+            "type": "package",
+            "package": {
+                "name": "jquery/geocomplete",
+                "version": "1.7.0",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "jquery.geocomplete"
+                },
+                "dist": {
+                    "url": "https://github.com/ubilabs/geocomplete/archive/1.7.0.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "jquery.hotkeys": {
+            "type": "package",
+            "package": {
+                "name": "jquery/hotkeys",
+                "version": "0.2.0",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "jquery.hotkeys"
+                },
+                "dist": {
+                    "url": "https://github.com/jeresig/jquery.hotkeys/archive/0.2.0.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "jquery.icheck": {
+            "type": "package",
+            "package": {
+                "name": "jquery/icheck",
+                "version": "1.0.2 ",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "jquery.icheck"
+                },
+                "dist": {
+                    "url": "https://github.com/fronteed/icheck/archive/1.0.2.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "jquery.image-picker": {
+            "type": "package",
+            "package": {
+                "name": "jquery/image-picker",
+                "version": "0.3.0",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "jquery.image-picker"
+                },
+                "dist": {
+                    "url": "https://github.com/rvera/image-picker/archive/0.3.0.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "jquery.inputmask": {
+            "type": "package",
+            "package": {
+                "name": "jquery/inputmask",
+                "version": "4.0.6",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "jquery.inputmask"
+                },
+                "dist": {
+                    "url": "https://github.com/RobinHerbots/jquery.inputmask/archive/4.0.6.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "jquery.intl-tel-input": {
+            "type": "package",
+            "package": {
+                "name": "jquery/intl-tel-input",
+                "version": "15.0.1",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "jquery.intl-tel-input"
+                },
+                "dist": {
+                    "url": "https://github.com/jackocnr/intl-tel-input/archive/v15.0.1.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "jquery.rateit": {
+            "type": "package",
+            "package": {
+                "name": "jquery/rateit",
+                "version": "1.1.1",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "jquery.rateit"
+                },
+                "dist": {
+                    "url": "https://github.com/gjunge/rateit.js/archive/1.1.1.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "jquery.select2": {
+            "type": "package",
+            "package": {
+                "name": "jquery/select2",
+                "version": "4.0.5",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "jquery.select2"
+                },
+                "dist": {
+                    "url": "https://github.com/select2/select2/archive/4.0.5.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "jquery.textcounter": {
+            "type": "package",
+            "package": {
+                "name": "jquery/textcounter",
+                "version": "0.8.0",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "jquery.textcounter"
+                },
+                "dist": {
+                    "url": "https://github.com/ractoon/jQuery-Text-Counter/archive/0.8.0.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "jquery.timepicker": {
+            "type": "package",
+            "package": {
+                "name": "jquery/timepicker",
+                "version": "1.11.14",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "jquery.timepicker"
+                },
+                "dist": {
+                    "url": "https://github.com/jonthornton/jquery-timepicker/archive/1.11.14.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "jquery.toggles": {
+            "type": "package",
+            "package": {
+                "name": "jquery/toggles",
+                "version": "4.0.0",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "jquery.toggles"
+                },
+                "dist": {
+                    "url": "https://github.com/simontabor/jquery-toggles/archive/v4.0.0.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "progress-tracker": {
+            "type": "package",
+            "package": {
+                "name": "progress-tracker/progress-tracker",
+                "version": "1.4.0",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "progress-tracker"
+                },
+                "dist": {
+                    "url": "https://github.com/NigelOToole/progress-tracker/archive/v1.4.0.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        },
+        "signature_pad": {
+            "type": "package",
+            "package": {
+                "name": "signature_pad/signature_pad",
+                "version": "2.3.0",
+                "type": "drupal-library",
+                "extra": {
+                    "installer-name": "signature_pad"
+                },
+                "dist": {
+                    "url": "https://github.com/szimek/signature_pad/archive/v2.3.0.zip",
+                    "type": "zip"
+                },
+                "require": {
+                    "composer/installers": "~1.0"
+                }
+            }
+        }
+    },
+    "require": {
+        "algolia/places": "1.16.1",
+        "ckeditor/autogrow": "4.10.1",
+        "ckeditor/codemirror": "v1.17.7",
+        "ckeditor/fakeobjects": "4.10.1",
+        "ckeditor/image": "4.10.1",
+        "ckeditor/link": "4.10.1",
+        "codemirror/codemirror": "5.44.0",
+        "jquery/chosen": "1.8.7",
+        "jquery/geocomplete": "1.7.0",
+        "jquery/hotkeys": "0.2.0",
+        "jquery/icheck": "1.0.2 ",
+        "jquery/image-picker": "0.3.0",
+        "jquery/inputmask": "4.0.6",
+        "jquery/intl-tel-input": "15.0.1",
+        "jquery/rateit": "1.1.1",
+        "jquery/select2": "4.0.5",
+        "jquery/textcounter": "0.8.0",
+        "jquery/timepicker": "1.11.14",
+        "jquery/toggles": "4.0.0",
+        "progress-tracker/progress-tracker": "1.4.0",
+        "signature_pad/signature_pad": "2.3.0"
+    }
+}
diff --git a/web/modules/webform/config/install/webform.settings.yml b/web/modules/webform/config/install/webform.settings.yml
index 01a48cc7a2dbbdc723ad50b147f30809ea300f83..d8f6d950819e6ac539c1a9f3c12693cc25dbb787 100644
--- a/web/modules/webform/config/install/webform.settings.yml
+++ b/web/modules/webform/config/install/webform.settings.yml
@@ -1,13 +1,14 @@
 settings:
+  default_status: open
   default_page_base_path: form
   default_form_open_message: 'This form has not yet been opened to submissions.'
-  default_form_close_message: 'Sorry...This form is closed to new submissions.'
+  default_form_close_message: 'Sorry…This form is closed to new submissions.'
   default_form_exception_message: 'Unable to display this webform. Please contact the site administrator.'
   default_submit_button_label: Submit
   default_reset_button_label: Reset
   default_form_submit_once: false
   default_form_confidential_message: 'This form is confidential. You must <a href="[site:login-url]/logout?destination=[current-page:url:relative]">Log out</a> to submit it.'
-  default_form_login_message: 'Please login to access this form.'
+  default_form_access_denied_message: 'Please login to access this form.'
   default_form_disable_back: false
   default_form_submit_back: false
   default_form_unsaved: false
@@ -16,12 +17,14 @@ settings:
   default_form_required: false
   default_form_required_label: 'Indicates required field'
   default_form_details_toggle: true
+  default_form_file_limit: ''
   form_classes: |
     container-inline clearfix
     form--inline clearfix
     messages messages--error
     messages messages--warning
     messages messages--status
+    
   button_classes: ''
   default_wizard_prev_button_label: '< Previous Page'
   default_wizard_next_button_label: 'Next Page >'
@@ -38,29 +41,59 @@ settings:
   default_confirmation_message: 'New submission added to [webform:title].'
   default_confirmation_back_label: 'Back to form'
   default_submission_label: '[webform_submission:submitted-to]: Submission #[webform_submission:serial]'
-  default_submission_login_message: 'Please login to access this submission.'
+  default_submission_access_denied_message: 'Please login to access this submission.'
   default_submission_exception_message: 'Unable to process this submission. Please contact the site administrator.'
   default_submission_locked_message: 'This submission has been locked.'
   default_submission_log: false
+  default_submission_views: {  }
+  default_submission_views_replace:
+    global_routes:
+      - entity.webform_submission.collection
+      - entity.webform_submission.user
+    webform_routes:
+      - entity.webform.results_submissions
+      - entity.webform.user.drafts
+      - entity.webform.user.submissions
+    node_routes:
+      - entity.node.webform.results_submissions
+      - entity.node.webform.user.drafts
+      - entity.node.webform.user.submissions
+  default_previous_submission_message: 'You have already submitted this webform. <a href="#">View your previous submission</a>.'
+  default_previous_submissions_message: 'You have already submitted this webform. <a href="#">View your previous submissions</a>.'
   default_autofill_message: 'This submission has been autofilled with your previous submission.'
   preview_classes: |
     messages messages--error
     messages messages--warning
     messages messages--status
+    
   confirmation_classes: |
     messages messages--error
     messages messages--warning
     messages messages--status
+    
   confirmation_back_classes: |
     button
+    
   default_limit_total_message: 'No more submissions are permitted.'
   default_limit_user_message: 'No more submissions are permitted.'
+  dialog: false
+  dialog_options:
+    narrow:
+      title: Narrow
+      width: 600
+    normal:
+      title: Normal
+      width: 800
+    wide:
+      title: Wide
+      width: 1000
 assets:
   css: ''
   javascript: ''
 handler:
   excluded_handlers: {  }
 export:
+  temp_directory: ''
   exporter: delimited
   multiple_delimiter: ;
   header_format: label
@@ -82,6 +115,7 @@ export:
   excluded_exporters: {  }
 batch:
   default_batch_export_size: 500
+  default_batch_import_size: 100
   default_batch_update_size: 500
   default_batch_delete_size: 500
   default_batch_email_size: 500
@@ -96,12 +130,14 @@ element:
     messages messages--error
     messages messages--warning
     messages messages--status
+    
   classes: |
     container-inline clearfix
     form--inline clearfix
     messages messages--error
     messages messages--warning
     messages messages--status
+    
   horizontal_rule_classes: |
     webform-horizontal-rule--solid
     webform-horizontal-rule--dashed
@@ -112,6 +148,7 @@ element:
     webform-horizontal-rule--thick
     webform-horizontal-rule--flaired
     webform-horizontal-rule--glyph
+    
   default_description_display: ''
   default_more_title: More
   default_section_title_tag: h2
@@ -120,13 +157,18 @@ element:
   default_empty_option_optional: ''
   default_icheck: ''
   default_google_maps_api_key: ''
+  default_algolia_places_app_id: ''
+  default_algolia_places_api_key: ''
   excluded_elements:
     password: password
     password_confirm: password_confirm
+    webform_location_geocomplete: webform_location_geocomplete
 html_editor:
   disabled: false
-  format: ''
+  element_format: ''
+  mail_format: ''
   tidy: true
+  make_unused_managed_files_temporary: true
 file:
   file_public: false
   file_private_redirect: true
@@ -135,7 +177,7 @@ file:
   default_managed_file_extensions: 'gif jpg png bmp eps tif pict psd txt rtf html odf pdf doc docx ppt pptx xls xlsx xml avi mov mp3 ogg wav bz2 dmg gz jar rar sit svg tar zip'
   default_audio_file_extensions: 'mp3 ogg wav'
   default_document_file_extensions: 'txt rtf pdf doc docx odt ppt pptx odp xls xlsx ods'
-  default_image_file_extensions: 'gif jpg png svg'
+  default_image_file_extensions: 'gif jpg png'
   default_video_file_extensions: 'avi mov mp4 ogg wav webm'
   make_unused_managed_files_temporary: true
   delete_temporary_managed_files: true
@@ -155,11 +197,13 @@ mail:
     
     Submitted values are:
     [webform_submission:values]
+    
   default_body_html: |
     <p>Submitted on [webform_submission:created]</p>
     <p>Submitted by: [webform_submission:user]</p>
     <p>Submitted values are:</p>
     [webform_submission:values]
+    
   roles: {  }
 test:
   types: |
@@ -199,13 +243,10 @@ test:
       - 'random@random.com'
     webform_email_multiple:
       - 'example@example.com, test@test.com, random@random.com'
-    webform_location:
-      - value: 'The White House, 1600 Pennsylvania Ave NW, Washington, DC 20500, USA'
-      - value: 'London SW1A 1AA, United Kingdom'
-      - value: 'Moscow, Russia, 10307'
     webform_time:
       - '09:00'
       - '17:00'
+    
   names: |
     first_name:
       - 'John'
@@ -245,9 +286,11 @@ test:
       - 'Loremipsum'
       - 'Oratione'
       - 'Dixisset'
+    
 ui:
   video_display: dialog
   details_save: true
+  help_disabled: false
   dialog_disabled: false
   offcanvas_disabled: false
   contribute_disabled: false
@@ -257,6 +300,7 @@ libraries:
   excluded_libraries:
     - jquery.chosen
     - jquery.icheck
+    - jquery.toggles
 requirements:
   cdn: true
   bootstrap: true
@@ -265,4 +309,6 @@ contribute:
   account_type: user
   account_id: null
 langcode: en
-third_party_settings: {  }
+third_party_settings:
+  captcha:
+    replace_administration_mode: true
diff --git a/web/modules/webform/config/install/webform.webform.contact.yml b/web/modules/webform/config/install/webform.webform.contact.yml
index 907046d3462ec798232011f30ab34b3c4ad56c76..4bfa609398cca91834d4842f36169498cbb8f185 100644
--- a/web/modules/webform/config/install/webform.webform.contact.yml
+++ b/web/modules/webform/config/install/webform.webform.contact.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: contact
 title: Contact
 description: 'Basic email contact webform.'
@@ -17,12 +19,12 @@ elements: |
     '#title': 'Your Name'
     '#type': textfield
     '#required': true
-    '#default_value': '[webform-authenticated-user:display-name]'
+    '#default_value': '[current-user:display-name]'
   email:
     '#title': 'Your Email'
     '#type': email
     '#required': true
-    '#default_value': '[webform-authenticated-user:mail]'
+    '#default_value': '[current-user:mail]'
   subject:
     '#title': Subject
     '#type': textfield
@@ -45,6 +47,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -52,6 +55,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -67,22 +71,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -93,6 +112,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -111,9 +131,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -166,6 +188,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_confirmation:
     id: email
@@ -183,17 +209,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: '[webform_submission:values:subject:raw]'
       body: '[webform_submission:values:message:value]'
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
@@ -209,7 +237,7 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
@@ -223,9 +251,11 @@ handlers:
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/config/install/webform.webform_options.gender.yml b/web/modules/webform/config/install/webform.webform_options.gender.yml
index 52669de9c7341e5c74004cd0d7ec485a7ff1858d..47d9f18f8d0ed190233c68db4d218d5564da7a59 100644
--- a/web/modules/webform/config/install/webform.webform_options.gender.yml
+++ b/web/modules/webform/config/install/webform.webform_options.gender.yml
@@ -11,4 +11,3 @@ likert: false
 options: |
   Male: Male
   Female: Female
-  Transgender: Transgender
diff --git a/web/modules/webform/config/install/webform.webform_options.state_province_codes.yml b/web/modules/webform/config/install/webform.webform_options.state_province_codes.yml
index 61c9feca0892ff55634350de868d9aafaf5a5870..d30fa1423a38c046b7b514fc6214a610ccc2e031 100644
--- a/web/modules/webform/config/install/webform.webform_options.state_province_codes.yml
+++ b/web/modules/webform/config/install/webform.webform_options.state_province_codes.yml
@@ -14,7 +14,7 @@ options: |
   AS: 'American Samoa'
   AZ: Arizona
   AR: Arkansas
-  AE: 'Armed Forces (Canada, Europe, Africa, or Middle East'
+  AE: 'Armed Forces (Canada, Europe, Africa, or Middle East)'
   AA: 'Armed Forces Americas'
   AP: 'Armed Forces Pacific'
   CA: California
@@ -22,7 +22,7 @@ options: |
   CT: Connecticut
   DE: Delaware
   DC: 'District of Columbia'
-  FM: 'Federate States of Micronesia'
+  FM: 'Federated States of Micronesia'
   FL: Florida
   GA: Georgia
   GU: Guam
diff --git a/web/modules/webform/config/install/webform.webform_options.state_province_names.yml b/web/modules/webform/config/install/webform.webform_options.state_province_names.yml
index cb000a3abc84430133c29f065d56ea9b28fed515..50708b088dc9dd0cb9bee0624141a67fef181c84 100644
--- a/web/modules/webform/config/install/webform.webform_options.state_province_names.yml
+++ b/web/modules/webform/config/install/webform.webform_options.state_province_names.yml
@@ -22,7 +22,7 @@ options: |
   Connecticut: Connecticut
   Delaware: Delaware
   'District of Columbia': 'District of Columbia'
-  'Federate States of Micronesia': 'Federate States of Micronesia'
+  'Federated States of Micronesia': 'Federated States of Micronesia'
   Florida: Florida
   Georgia: Georgia
   Guam: Guam
diff --git a/web/modules/webform/config/install/webform.webform_options.translations.yml b/web/modules/webform/config/install/webform.webform_options.translations.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9f87b76f13ac6e114eeceaa95260894dbf059b43
--- /dev/null
+++ b/web/modules/webform/config/install/webform.webform_options.translations.yml
@@ -0,0 +1,11 @@
+langcode: en
+status: true
+dependencies:
+  enforced:
+    module:
+      - webform
+id: translations
+label: Translations
+category: Language
+likert: false
+options: ''
diff --git a/web/modules/webform/config/optional/views.view.webform_submissions.yml b/web/modules/webform/config/optional/views.view.webform_submissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..27283e5b57b29479640fc5a7d1663d0e91098764
--- /dev/null
+++ b/web/modules/webform/config/optional/views.view.webform_submissions.yml
@@ -0,0 +1,3167 @@
+langcode: en
+status: true
+dependencies:
+  enforced:
+    module:
+      - webform
+  module:
+    - user
+    - webform
+id: webform_submissions
+label: 'Webform submissions'
+module: views
+description: 'Default webform submissions views.'
+tag: ''
+base_table: webform_submission
+base_field: sid
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: none
+        options: {  }
+      cache:
+        type: tag
+        options: {  }
+      query:
+        type: views_query
+        options:
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_comment: ''
+          query_tags: {  }
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      pager:
+        type: full
+        options:
+          items_per_page: 25
+          offset: 0
+          id: 0
+          total_pages: null
+          tags:
+            previous: ‹‹
+            next: ››
+            first: '« First'
+            last: 'Last »'
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50, 100, 200'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+          quantity: 9
+      style:
+        type: table
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          override: true
+          sticky: false
+          caption: ''
+          summary: ''
+          description: ''
+          columns:
+            webform_submission_bulk_form: webform_submission_bulk_form
+            sid: sid
+            in_draft: in_draft
+            sticky: sticky
+            locked: locked
+            created: created
+            completed: completed
+            remote_addr: remote_addr
+            view_webform_submission: view_webform_submission
+            edit_webform_submission: view_webform_submission
+          info:
+            webform_submission_bulk_form:
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            sid:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            in_draft:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            sticky:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            locked:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            created:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            completed:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            remote_addr:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            view_webform_submission:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ' '
+              empty_column: false
+              responsive: ''
+            edit_webform_submission:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+          default: created
+          empty_table: false
+      row:
+        type: fields
+        options:
+          inline: {  }
+          separator: ''
+          hide_empty: false
+          default_field_elements: true
+      fields:
+        sid:
+          id: sid
+          table: webform_submission
+          field: sid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: '#'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: number_integer
+          settings:
+            thousand_separator: ''
+            prefix_suffix: true
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: sid
+          plugin_id: field
+        in_draft:
+          id: in_draft
+          table: webform_submission
+          field: in_draft
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Draft
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: yes-no
+            format_custom_true: ''
+            format_custom_false: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: in_draft
+          plugin_id: field
+        created:
+          id: created
+          table: webform_submission
+          field: created
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Created
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: timestamp
+          settings:
+            date_format: medium
+            custom_date_format: ''
+            timezone: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: created
+          plugin_id: field
+        remote_addr:
+          id: remote_addr
+          table: webform_submission
+          field: remote_addr
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'IP address'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: remote_addr
+          plugin_id: field
+        view_webform_submission:
+          id: view_webform_submission
+          table: webform_submission
+          field: view_webform_submission
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Operations
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          text: view
+          output_url_as_text: false
+          absolute: false
+          entity_type: webform_submission
+          plugin_id: entity_link
+      filters: {  }
+      sorts: {  }
+      header:
+        result:
+          id: result
+          table: views
+          field: result
+          relationship: none
+          group_type: group
+          admin_label: ''
+          empty: false
+          content: 'Displaying @start - @end of @total'
+          plugin_id: result
+      footer: {  }
+      empty:
+        area_text_custom:
+          id: area_text_custom
+          table: views
+          field: area_text_custom
+          relationship: none
+          group_type: group
+          admin_label: ''
+          empty: true
+          tokenize: false
+          content: 'No submissions available.'
+          plugin_id: text_custom
+      relationships: {  }
+      arguments:
+        webform_id:
+          id: webform_id
+          table: webform_submission
+          field: webform_id
+          entity_type: webform_submission
+          entity_field: webform_id
+          plugin_id: string
+        entity_type:
+          id: entity_type
+          table: webform_submission
+          field: entity_type
+          entity_type: webform_submission
+          entity_field: entity_type
+          plugin_id: string
+        entity_id:
+          id: entity_id
+          table: webform_submission
+          field: entity_id
+          entity_type: webform_submission
+          entity_field: entity_id
+          plugin_id: string
+        uid:
+          id: uid
+          table: webform_submission
+          field: uid
+          entity_type: webform_submission
+          entity_field: uid
+          plugin_id: numeric
+        in_draft:
+          id: in_draft
+          table: webform_submission
+          field: in_draft
+          relationship: none
+          group_type: group
+          admin_label: ''
+          default_action: ignore
+          exception:
+            value: all
+            title_enable: false
+            title: All
+          title_enable: false
+          title: ''
+          default_argument_type: fixed
+          default_argument_options:
+            argument: ''
+          default_argument_skip_url: false
+          summary_options:
+            base_path: ''
+            count: true
+            items_per_page: 25
+            override: false
+          summary:
+            sort_order: asc
+            number_of_records: 0
+            format: default_summary
+          specify_validation: false
+          validate:
+            type: none
+            fail: 'not found'
+          validate_options: {  }
+          break_phrase: false
+          not: false
+          entity_type: webform_submission
+          entity_field: in_draft
+          plugin_id: numeric
+      display_extenders: {  }
+      filter_groups:
+        operator: AND
+        groups: {  }
+      title: 'Webform submissions'
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - user
+      tags: {  }
+  embed_default:
+    display_plugin: embed
+    id: embed_default
+    display_title: 'Embed: Default'
+    position: 1
+    display_options:
+      display_extenders: {  }
+      display_description: 'Display submissions.'
+      defaults:
+        fields: true
+        style: false
+        row: false
+        filters: true
+        filter_groups: true
+      style:
+        type: table
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          override: true
+          sticky: false
+          caption: ''
+          summary: ''
+          description: ''
+          columns:
+            sid: sid
+            in_draft: in_draft
+            created: created
+            remote_addr: remote_addr
+            view_webform_submission: view_webform_submission
+          info:
+            sid:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            in_draft:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            created:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            remote_addr:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            view_webform_submission:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ' '
+              empty_column: false
+              responsive: ''
+          default: created
+          empty_table: false
+      row:
+        type: fields
+        options:
+          inline: {  }
+          separator: ''
+          hide_empty: false
+          default_field_elements: true
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - user
+      tags: {  }
+  embed_administer:
+    display_plugin: embed
+    id: embed_administer
+    display_title: 'Embed: Administer'
+    position: 2
+    display_options:
+      display_extenders: {  }
+      display_description: 'Administer submissions.'
+      style:
+        type: table
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          override: true
+          sticky: false
+          caption: ''
+          summary: ''
+          description: ''
+          columns:
+            webform_submission_bulk_form: webform_submission_bulk_form
+            sid: sid
+            in_draft: in_draft
+            sticky: sticky
+            locked: locked
+            created: created
+            completed: completed
+            remote_addr: remote_addr
+            view_webform_submission: view_webform_submission
+            edit_webform_submission: view_webform_submission
+          info:
+            webform_submission_bulk_form:
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            sid:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            in_draft:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            sticky:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            locked:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            created:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            completed:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            remote_addr:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            view_webform_submission:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ' '
+              empty_column: false
+              responsive: ''
+            edit_webform_submission:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+          default: created
+          empty_table: false
+      defaults:
+        style: false
+        row: false
+        access: false
+        fields: false
+        filters: false
+        filter_groups: false
+      row:
+        type: fields
+        options:
+          inline: {  }
+          separator: ''
+          hide_empty: false
+          default_field_elements: true
+      access:
+        type: perm
+        options:
+          perm: 'administer webform submission'
+      fields:
+        webform_submission_bulk_form:
+          id: webform_submission_bulk_form
+          table: webform_submission
+          field: webform_submission_bulk_form
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'Webform submission operations bulk form'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          action_title: Action
+          include_exclude: exclude
+          selected_actions: {  }
+          entity_type: webform_submission
+          plugin_id: webform_submission_bulk_form
+        sid:
+          id: sid
+          table: webform_submission
+          field: sid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: '#'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: number_integer
+          settings:
+            thousand_separator: ''
+            prefix_suffix: true
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: sid
+          plugin_id: field
+        in_draft:
+          id: in_draft
+          table: webform_submission
+          field: in_draft
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Draft
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: yes-no
+            format_custom_true: ''
+            format_custom_false: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: in_draft
+          plugin_id: field
+        sticky:
+          id: sticky
+          table: webform_submission
+          field: sticky
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Sticky
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: yes-no
+            format_custom_true: ''
+            format_custom_false: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: sticky
+          plugin_id: field
+        locked:
+          id: locked
+          table: webform_submission
+          field: locked
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Locked
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: yes-no
+            format_custom_true: ''
+            format_custom_false: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: locked
+          plugin_id: field
+        created:
+          id: created
+          table: webform_submission
+          field: created
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Created
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: timestamp
+          settings:
+            date_format: medium
+            custom_date_format: ''
+            timezone: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: created
+          plugin_id: field
+        completed:
+          id: completed
+          table: webform_submission
+          field: completed
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Completed
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: timestamp
+          settings:
+            date_format: medium
+            custom_date_format: ''
+            timezone: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: completed
+          plugin_id: field
+        remote_addr:
+          id: remote_addr
+          table: webform_submission
+          field: remote_addr
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'IP address'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: remote_addr
+          plugin_id: field
+        operations:
+          id: operations
+          table: webform_submission
+          field: operations
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Operations
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          destination: true
+          entity_type: null
+          entity_field: null
+          plugin_id: entity_operations
+      filters:
+        in_draft:
+          id: in_draft
+          table: webform_submission
+          field: in_draft
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: '='
+          value: All
+          group: 1
+          exposed: true
+          expose:
+            operator_id: ''
+            label: 'Is draft'
+            description: ''
+            use_operator: false
+            operator: in_draft_op
+            identifier: in_draft
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          entity_type: webform_submission
+          entity_field: in_draft
+          plugin_id: boolean
+        sticky:
+          id: sticky
+          table: webform_submission
+          field: sticky
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: '='
+          value: All
+          group: 1
+          exposed: true
+          expose:
+            operator_id: ''
+            label: Sticky
+            description: ''
+            use_operator: false
+            operator: sticky_op
+            identifier: sticky
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          entity_type: webform_submission
+          entity_field: sticky
+          plugin_id: boolean
+        locked:
+          id: locked
+          table: webform_submission
+          field: locked
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: '='
+          value: All
+          group: 1
+          exposed: true
+          expose:
+            operator_id: ''
+            label: Locked
+            description: ''
+            use_operator: false
+            operator: locked_op
+            identifier: locked
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+              demo_region: '0'
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          entity_type: webform_submission
+          entity_field: locked
+          plugin_id: boolean
+      filter_groups:
+        operator: AND
+        groups:
+          1: AND
+    cache_metadata:
+      max-age: 0
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - user
+        - user.permissions
+      tags: {  }
+  embed_manage:
+    display_plugin: embed
+    id: embed_manage
+    display_title: 'Embed: Manage'
+    position: 3
+    display_options:
+      display_extenders: {  }
+      display_description: 'Manage submissions.'
+      fields:
+        webform_submission_bulk_form:
+          id: webform_submission_bulk_form
+          table: webform_submission
+          field: webform_submission_bulk_form
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'Webform submission operations bulk form'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          action_title: Action
+          include_exclude: include
+          selected_actions:
+            - webform_submission_make_lock_action
+            - webform_submission_make_sticky_action
+            - webform_submission_make_unlock_action
+            - webform_submission_make_unsticky_action
+          entity_type: webform_submission
+          plugin_id: webform_submission_bulk_form
+        sid:
+          id: sid
+          table: webform_submission
+          field: sid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: '#'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: number_integer
+          settings:
+            thousand_separator: ''
+            prefix_suffix: true
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: sid
+          plugin_id: field
+        in_draft:
+          id: in_draft
+          table: webform_submission
+          field: in_draft
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Draft
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: yes-no
+            format_custom_true: ''
+            format_custom_false: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: in_draft
+          plugin_id: field
+        sticky:
+          id: sticky
+          table: webform_submission
+          field: sticky
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Sticky
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: yes-no
+            format_custom_true: ''
+            format_custom_false: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: sticky
+          plugin_id: field
+        locked:
+          id: locked
+          table: webform_submission
+          field: locked
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Locked
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: yes-no
+            format_custom_true: ''
+            format_custom_false: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: locked
+          plugin_id: field
+        created:
+          id: created
+          table: webform_submission
+          field: created
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Created
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: timestamp
+          settings:
+            date_format: medium
+            custom_date_format: ''
+            timezone: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: created
+          plugin_id: field
+        completed:
+          id: completed
+          table: webform_submission
+          field: completed
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Completed
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: timestamp
+          settings:
+            date_format: medium
+            custom_date_format: ''
+            timezone: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: completed
+          plugin_id: field
+        remote_addr:
+          id: remote_addr
+          table: webform_submission
+          field: remote_addr
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'IP address'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: remote_addr
+          plugin_id: field
+        view_webform_submission:
+          id: view_webform_submission
+          table: webform_submission
+          field: view_webform_submission
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Operations
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          text: view
+          output_url_as_text: false
+          absolute: false
+          entity_type: webform_submission
+          plugin_id: entity_link
+        edit_webform_submission:
+          id: edit_webform_submission
+          table: webform_submission
+          field: edit_webform_submission
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Operations
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          text: edit
+          output_url_as_text: false
+          absolute: false
+          entity_type: webform_submission
+          plugin_id: entity_link_edit
+      defaults:
+        fields: false
+        style: false
+        row: false
+        access: false
+        filters: false
+        filter_groups: false
+      style:
+        type: table
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          override: true
+          sticky: false
+          caption: ''
+          summary: ''
+          description: ''
+          columns:
+            webform_submission_bulk_form: webform_submission_bulk_form
+            sid: sid
+            in_draft: in_draft
+            sticky: sticky
+            locked: locked
+            created: created
+            completed: completed
+            remote_addr: remote_addr
+            view_webform_submission: view_webform_submission
+            edit_webform_submission: view_webform_submission
+          info:
+            webform_submission_bulk_form:
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            sid:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            in_draft:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            sticky:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            locked:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            created:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            completed:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            remote_addr:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            view_webform_submission:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ' '
+              empty_column: false
+              responsive: ''
+            edit_webform_submission:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+          default: created
+          empty_table: false
+      row:
+        type: fields
+        options:
+          inline: {  }
+          separator: ''
+          hide_empty: false
+          default_field_elements: true
+      access:
+        type: perm
+        options:
+          perm: 'edit any webform submission'
+      filters:
+        in_draft:
+          id: in_draft
+          table: webform_submission
+          field: in_draft
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: '='
+          value: All
+          group: 1
+          exposed: true
+          expose:
+            operator_id: ''
+            label: 'Is draft'
+            description: ''
+            use_operator: false
+            operator: in_draft_op
+            identifier: in_draft
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          entity_type: webform_submission
+          entity_field: in_draft
+          plugin_id: boolean
+        sticky:
+          id: sticky
+          table: webform_submission
+          field: sticky
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: '='
+          value: All
+          group: 1
+          exposed: true
+          expose:
+            operator_id: ''
+            label: Sticky
+            description: ''
+            use_operator: false
+            operator: sticky_op
+            identifier: sticky
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          entity_type: webform_submission
+          entity_field: sticky
+          plugin_id: boolean
+        locked:
+          id: locked
+          table: webform_submission
+          field: locked
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: '='
+          value: All
+          group: 1
+          exposed: true
+          expose:
+            operator_id: ''
+            label: Locked
+            description: ''
+            use_operator: false
+            operator: locked_op
+            identifier: locked
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+              demo_region: '0'
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          entity_type: webform_submission
+          entity_field: locked
+          plugin_id: boolean
+      filter_groups:
+        operator: AND
+        groups:
+          1: AND
+    cache_metadata:
+      max-age: 0
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - user
+        - user.permissions
+      tags: {  }
+  embed_review:
+    display_plugin: embed
+    id: embed_review
+    display_title: 'Embed: Review'
+    position: 4
+    display_options:
+      display_extenders: {  }
+      display_description: 'Review submissions.'
+      fields:
+        sid:
+          id: sid
+          table: webform_submission
+          field: sid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: '#'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: number_integer
+          settings:
+            thousand_separator: ''
+            prefix_suffix: true
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: sid
+          plugin_id: field
+        in_draft:
+          id: in_draft
+          table: webform_submission
+          field: in_draft
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Draft
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: yes-no
+            format_custom_true: ''
+            format_custom_false: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: in_draft
+          plugin_id: field
+        sticky:
+          id: sticky
+          table: webform_submission
+          field: sticky
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Sticky
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: yes-no
+            format_custom_true: ''
+            format_custom_false: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: sticky
+          plugin_id: field
+        locked:
+          id: locked
+          table: webform_submission
+          field: locked
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Locked
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: yes-no
+            format_custom_true: ''
+            format_custom_false: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: locked
+          plugin_id: field
+        created:
+          id: created
+          table: webform_submission
+          field: created
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Created
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: timestamp
+          settings:
+            date_format: medium
+            custom_date_format: ''
+            timezone: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: created
+          plugin_id: field
+        completed:
+          id: completed
+          table: webform_submission
+          field: completed
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Completed
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: timestamp
+          settings:
+            date_format: medium
+            custom_date_format: ''
+            timezone: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: completed
+          plugin_id: field
+        remote_addr:
+          id: remote_addr
+          table: webform_submission
+          field: remote_addr
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'IP address'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: remote_addr
+          plugin_id: field
+        view_webform_submission:
+          id: view_webform_submission
+          table: webform_submission
+          field: view_webform_submission
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Operations
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          text: view
+          output_url_as_text: false
+          absolute: false
+          entity_type: webform_submission
+          plugin_id: entity_link
+      defaults:
+        fields: false
+        style: false
+        row: false
+        access: false
+        filters: false
+        filter_groups: false
+      style:
+        type: table
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          override: true
+          sticky: false
+          caption: ''
+          summary: ''
+          description: ''
+          columns:
+            webform_submission_bulk_form: webform_submission_bulk_form
+            sid: sid
+            in_draft: in_draft
+            sticky: sticky
+            locked: locked
+            created: created
+            completed: completed
+            remote_addr: remote_addr
+            view_webform_submission: view_webform_submission
+            edit_webform_submission: view_webform_submission
+          info:
+            webform_submission_bulk_form:
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            sid:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            in_draft:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            sticky:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            locked:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            created:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            completed:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            remote_addr:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            view_webform_submission:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ' '
+              empty_column: false
+              responsive: ''
+            edit_webform_submission:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+          default: created
+          empty_table: false
+      row:
+        type: fields
+        options:
+          inline: {  }
+          separator: ''
+          hide_empty: false
+          default_field_elements: true
+      access:
+        type: perm
+        options:
+          perm: 'view any webform submission'
+      filters:
+        in_draft:
+          id: in_draft
+          table: webform_submission
+          field: in_draft
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: '='
+          value: All
+          group: 1
+          exposed: true
+          expose:
+            operator_id: ''
+            label: 'Is draft'
+            description: ''
+            use_operator: false
+            operator: in_draft_op
+            identifier: in_draft
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          entity_type: webform_submission
+          entity_field: in_draft
+          plugin_id: boolean
+        sticky:
+          id: sticky
+          table: webform_submission
+          field: sticky
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: '='
+          value: All
+          group: 1
+          exposed: true
+          expose:
+            operator_id: ''
+            label: Sticky
+            description: ''
+            use_operator: false
+            operator: sticky_op
+            identifier: sticky
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          entity_type: webform_submission
+          entity_field: sticky
+          plugin_id: boolean
+        locked:
+          id: locked
+          table: webform_submission
+          field: locked
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: '='
+          value: All
+          group: 1
+          exposed: true
+          expose:
+            operator_id: ''
+            label: Locked
+            description: ''
+            use_operator: false
+            operator: locked_op
+            identifier: locked
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+              demo_region: '0'
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          entity_type: webform_submission
+          entity_field: locked
+          plugin_id: boolean
+      filter_groups:
+        operator: AND
+        groups:
+          1: AND
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - user
+        - user.permissions
+      tags: {  }
diff --git a/web/modules/webform/config/schema/webform.action.schema.yml b/web/modules/webform/config/schema/webform.action.schema.yml
index 0cf20edbd7e4580ea92b300e7b4f1422bb34833d..4ebbd150577564f83d9fe3dea006b658a066c280 100644
--- a/web/modules/webform/config/schema/webform.action.schema.yml
+++ b/web/modules/webform/config/schema/webform.action.schema.yml
@@ -1,19 +1,15 @@
 action.configuration.webform_submission_delete_action:
   type: action_configuration_default
   label: 'Delete submission configuration'
-
 action.configuration.webform_submission_make_sticky_action:
   type: action_configuration_default
   label: 'Star/Flag selected submission configuration'
-
 action.configuration.webform_submission_make_unsticky_action:
   type: action_configuration_default
   label: 'Unstar/Unflag selected submission configuration'
-
 action.configuration.webform_submission_make_lock_action:
   type: action_configuration_default
   label: 'Lock selected submission configuration'
-
 action.configuration.webform_submission_make_unlock_action:
   type: action_configuration_default
   label: 'Unlock selected submission configuration'
diff --git a/web/modules/webform/config/schema/webform.block.schema.yml b/web/modules/webform/config/schema/webform.block.schema.yml
index 1b24a175e76ada77c75976673da739b8822fce5b..ebe1345221cbfb62cac066ba20bb6dbcf367f4d8 100644
--- a/web/modules/webform/config/schema/webform.block.schema.yml
+++ b/web/modules/webform/config/schema/webform.block.schema.yml
@@ -4,23 +4,25 @@ block.settings.webform_block:
   mapping:
     webform_id:
       type: string
-      label: 'Webform'
+      label: Webform
     default_data:
       type: text
       label: 'Default webform submission data'
-
+    redirect:
+      type: boolean
+      label: 'Redirect to the webform'
 block.settings.webform_submission_limit_block:
   type: block_settings
   label: 'Webform submission limits block'
   mapping:
     type:
       type: text
-      label: 'Type'
+      label: Type
     source_entity:
       type: boolean
       label: 'Source entity'
     content:
-      label: 'Content'
+      label: Content
       type: text
     progress_bar:
       type: boolean
@@ -33,7 +35,7 @@ block.settings.webform_submission_limit_block:
       label: 'Progress bar message'
     webform_id:
       type: string
-      label: 'Webform'
+      label: Webform
     entity_type:
       type: string
       label: 'Source entity type'
diff --git a/web/modules/webform/config/schema/webform.entity.webform.schema.yml b/web/modules/webform/config/schema/webform.entity.webform.schema.yml
index 3c751a4156398e1e5a347996de2cd82b7e5faa1b..804ebdb13c0007f09d7a6fc7c8a9710fed88a284 100644
--- a/web/modules/webform/config/schema/webform.entity.webform.schema.yml
+++ b/web/modules/webform/config/schema/webform.entity.webform.schema.yml
@@ -1,10 +1,13 @@
-webform.webform.*:
+'webform.webform.*':
   type: config_entity
-  label: 'Webforms'
+  label: Webforms
   mapping:
     status:
       type: string
-      label: 'Status'
+      label: Status
+    weight:
+      type: integer
+      label: Weight
     open:
       type: string
       label: 'Open date/time'
@@ -13,22 +16,25 @@ webform.webform.*:
       label: 'Close date/time'
     uid:
       type: integer
-      label: 'Author'
+      label: Author
     template:
       type: boolean
-      label: 'Template'
+      label: Template
+    archive:
+      type: boolean
+      label: Archive
     id:
       type: string
       label: 'Machine name'
     title:
       type: label
-      label: 'Title'
+      label: Title
     description:
       type: label
       label: 'Administrative description'
     category:
       type: label
-      label: 'Category'
+      label: Category
     elements:
       type: text
       label: 'Elements (YAML)'
@@ -37,12 +43,10 @@ webform.webform.*:
       label: 'CSS (Cascading Style Sheets)'
     javascript:
       type: string
-      label: 'JavaScript'
+      label: JavaScript
     settings:
       type: mapping
-      label: 'Settings'
-      # Below mapping is copied to: webform.handler.settings
-      #@see webform.plugin.handler.schema.yml
+      label: Settings
       mapping:
         ajax:
           type: boolean
@@ -59,6 +63,9 @@ webform.webform.*:
         page_confirm_path:
           type: string
           label: 'Page confirm URL alias'
+        form_title:
+          type: string
+          label: 'Form title display'
         form_submit_once:
           type: boolean
           label: 'Prevent duplicate submissions'
@@ -80,6 +87,9 @@ webform.webform.*:
         form_confidential_message:
           type: text
           label: 'Form confidential message'
+        form_remote_addr:
+          type: boolean
+          label: 'Track user IP address'
         form_convert_anonymous:
           type: boolean
           label: 'Convert anonymous drafts and submissions to authenticated'
@@ -118,19 +128,28 @@ webform.webform.*:
           label: 'Display required indicator'
         form_autofocus:
           type: boolean
-          label: 'Autofocus'
+          label: Autofocus
         form_details_toggle:
           type: boolean
           label: 'Display collapse/expand all details link'
         form_reset:
           type: boolean
           label: 'Display reset button'
-        form_login:
-          type: boolean
-          label: 'Redirect to login when access denied to webform'
-        form_login_message:
+        form_access_denied:
+          type: string
+          label: 'Form access denied action'
+        form_access_denied_title:
+          type: label
+          label: 'Form access denied title'
+        form_access_denied_message:
           type: text
-          label: 'Login message when access denied to webform'
+          label: 'Form access denied message'
+        form_access_denied_attributes:
+          type: ignore
+          label: 'Form access denied message attributes'
+        form_file_limit:
+          type: string
+          label: 'Form file upload limit'
         submission_label:
           type: label
           label: 'Default submission label'
@@ -143,18 +162,92 @@ webform.webform.*:
         submission_log:
           type: boolean
           label: 'Submission logging'
+        submission_excluded_elements:
+          type: sequence
+          label: 'Submission excluded elements'
+          sequence:
+            type: string
+            label: 'Element key'
+        submission_exclude_empty:
+          type: boolean
+          label: 'Submission exclude empty elements'
+        submission_exclude_empty_checkbox:
+          type: boolean
+          label: 'Submission exclude unselected checkboxes'
+        submission_views:
+          type: sequence
+          label: 'Submission views'
+          sequence:
+            type: mapping
+            label: 'Submission view'
+            mapping:
+              title:
+                type: text
+                label: Title
+              view:
+                type: string
+                label: 'View name / Display ID'
+              webform_routes:
+                type: sequence
+                label: 'Apply to webform'
+                sequence:
+                  type: string
+                  label: Route
+              node_routes:
+                type: sequence
+                label: 'Apply to node'
+                sequence:
+                  type: string
+                  label: Route
+        submission_views_replace:
+          type: mapping
+          label: 'Submission view replace'
+          mapping:
+            global_routes:
+              type: sequence
+              label: 'Replace to global'
+              sequence:
+                type: string
+                label: Route
+            webform_routes:
+              type: sequence
+              label: 'Replace to webform'
+              sequence:
+                type: string
+                label: Route
+            node_routes:
+              type: sequence
+              label: 'Replace to node'
+              sequence:
+                type: string
+                label: Route
         submission_user_columns:
           type: sequence
           label: 'Submission user columns'
           sequence:
             type: string
             label: 'Column name'
-        submission_login:
+        submission_user_duplicate:
           type: boolean
-          label: 'Redirect to login when access denied to submission'
-        submission_login_message:
+          label: 'Submission user duplicate'
+        submission_access_denied:
+          type: string
+          label: 'Submission access denied action'
+        submission_access_denied_title:
+          type: label
+          label: 'Submission access denied title'
+        submission_access_denied_message:
+          type: text
+          label: 'Submission access denied message'
+        submission_access_denied_attributes:
+          type: ignore
+          label: 'Submission access denied message attributes'
+        previous_submission_message:
           type: text
-          label: 'Login message when access denied to submission'
+          label: 'Previous submission message'
+        previous_submissions_message:
+          type: text
+          label: 'Previous submissions message'
         autofill:
           type: boolean
           label: 'Autofill with previous submission'
@@ -176,12 +269,18 @@ webform.webform.*:
         wizard_progress_percentage:
           type: boolean
           label: 'Show wizard progress pages'
+        wizard_progress_link:
+          type: boolean
+          label: 'Link to previous pages in progress bar'
         wizard_start_label:
           type: label
           label: 'Wizard start label'
         wizard_start_attributes:
           type: ignore
           label: 'Wizard start attributes'
+        wizard_preview_link:
+          type: boolean
+          label: 'Link to previous pages in preview'
         wizard_confirmation:
           type: boolean
           label: 'Include confirmation page in progress'
@@ -215,6 +314,9 @@ webform.webform.*:
         preview_exclude_empty:
           type: boolean
           label: 'Preview exclude empty elements'
+        preview_exclude_empty_checkbox:
+          type: boolean
+          label: 'Preview exclude unselected checkboxes'
         draft:
           type: string
           label: 'Allow users to save and finish the webform later.'
@@ -237,7 +339,7 @@ webform.webform.*:
           type: string
           label: 'Confirmation URL'
         confirmation_title:
-          type: text
+          type: label
           label: 'Confirmation title'
         confirmation_message:
           type: text
@@ -269,6 +371,9 @@ webform.webform.*:
         limit_total_message:
           type: text
           label: 'Limit total message'
+        limit_total_unique:
+          type: boolean
+          label: 'Limit total to one submission per source entity'
         limit_user:
           type: integer
           label: 'Limit user submissions'
@@ -278,6 +383,9 @@ webform.webform.*:
         limit_user_message:
           type: text
           label: 'Limit user message'
+        limit_user_unique:
+          type: boolean
+          label: 'Limit user to one submission per source entity'
         entity_limit_total:
           type: integer
           label: 'Entity limit total submissions'
@@ -306,229 +414,30 @@ webform.webform.*:
           type: boolean
           label: 'Allow updates using token'
     access:
-      type: mapping
-      label: 'Access'
-      mapping:
-        create:
-          type: mapping
-          label: 'Create webform submissions'
-          mapping:
-            roles:
-              type: sequence
-              label: 'Roles'
-              sequence:
-                type: string
-                label: 'Role'
-            users:
-              type: sequence
-              label: 'Users'
-              sequence:
-                type: integer
-                label: 'User IDs'
-            permissions:
-              type: sequence
-              label: 'Permissions'
-              sequence:
-                type: string
-                label: 'Permission'
-        view_any:
-          type: mapping
-          label: 'View any webform submissions'
-          mapping:
-            roles:
-              type: sequence
-              label: 'Roles'
-              sequence:
-                type: string
-                label: 'Role'
-            users:
-              type: sequence
-              label: 'Users'
-              sequence:
-                type: integer
-                label: 'User IDs'
-            permissions:
-              type: sequence
-              label: 'Permissions'
-              sequence:
-                type: string
-                label: 'Permission'
-        update_any:
-          type: mapping
-          label: 'Update any webform submissions'
-          mapping:
-            roles:
-              type: sequence
-              label: 'Roles'
-              sequence:
-                type: string
-                label: 'Role'
-            users:
-              type: sequence
-              label: 'Users'
-              sequence:
-                type: integer
-                label: 'User IDs'
-            permissions:
-              type: sequence
-              label: 'Permissions'
-              sequence:
-                type: string
-                label: 'Permission'
-        delete_any:
-          type: mapping
-          label: 'Delete any webform submissions'
-          mapping:
-            roles:
-              type: sequence
-              label: 'Roles'
-              sequence:
-                type: string
-                label: 'Role'
-            users:
-              type: sequence
-              label: 'Users'
-              sequence:
-                type: integer
-                label: 'User IDs'
-            permissions:
-              type: sequence
-              label: 'Permissions'
-              sequence:
-                type: string
-                label: 'Permission'
-        purge_any:
-          type: mapping
-          label: 'Purge any webform submissions'
-          mapping:
-            roles:
-              type: sequence
-              label: 'Roles'
-              sequence:
-                type: string
-                label: 'Role'
-            users:
-              type: sequence
-              label: 'Users'
-              sequence:
-                type: integer
-                label: 'User IDs'
-            permissions:
-              type: sequence
-              label: 'Permissions'
-              sequence:
-                type: string
-                label: 'Permission'
-        view_own:
-          type: mapping
-          label: 'View own webform submissions'
-          mapping:
-            roles:
-              type: sequence
-              label: 'Roles'
-              sequence:
-                type: string
-                label: 'Role'
-            users:
-              type: sequence
-              label: 'Users'
-              sequence:
-                type: integer
-                label: 'User IDs'
-            permissions:
-              type: sequence
-              label: 'Permissions'
-              sequence:
-                type: string
-                label: 'Permission'
-        update_own:
-          type: mapping
-          label: 'Update own webform submissions'
-          mapping:
-            roles:
-              type: sequence
-              label: 'Roles'
-              sequence:
-                type: string
-                label: 'Role'
-            users:
-              type: sequence
-              label: 'Users'
-              sequence:
-                type: integer
-                label: 'User IDs'
-            permissions:
-              type: sequence
-              label: 'Permissions'
-              sequence:
-                type: string
-                label: 'Permission'
-        delete_own:
-          type: mapping
-          label: 'Delete own webform submissions'
-          mapping:
-            roles:
-              type: sequence
-              label: 'Roles'
-              sequence:
-                type: string
-                label: 'Role'
-            users:
-              type: sequence
-              label: 'Users'
-              sequence:
-                type: integer
-                label: 'User IDs'
-            permissions:
-              type: sequence
-              label: 'Permissions'
-              sequence:
-                type: string
-                label: 'Permission'
-        administer:
-          type: mapping
-          label: 'Administer webform and submissions'
-          mapping:
-            roles:
-              type: sequence
-              label: 'Roles'
-              sequence:
-                type: string
-                label: 'Role'
-            users:
-              type: sequence
-              label: 'Users'
-              sequence:
-                type: integer
-                label: 'User IDs'
-            permissions:
-              type: sequence
-              label: 'Permissions'
-              sequence:
-                type: string
-                label: 'Permission'
-        test:
-          type: mapping
-          label: 'Test webform'
-          mapping:
-            roles:
-              type: sequence
-              label: 'Roles'
-              sequence:
-                type: string
-                label: 'Role'
-            users:
-              type: sequence
-              label: 'Users'
-              sequence:
-                type: integer
-                label: 'User IDs'
-            permissions:
-              type: sequence
-              label: 'Permissions'
-              sequence:
-                type: string
-                label: 'Permission'
+      type: sequence
+      label: 'Access Rules'
+      sequence:
+        type: mapping
+        label: 'Access Rule'
+        mapping:
+          roles:
+            type: sequence
+            label: Roles
+            sequence:
+              type: string
+              label: Role
+          users:
+            type: sequence
+            label: Users
+            sequence:
+              type: integer
+              label: 'User IDs'
+          permissions:
+            type: sequence
+            label: Permissions
+            sequence:
+              type: string
+              label: Permission
     handlers:
       type: sequence
       label: 'Webform handlers'
@@ -543,20 +452,20 @@ webform.webform.*:
             label: 'Handler instance ID'
           label:
             type: label
-            label: 'Label'
+            label: Label
           status:
             type: boolean
-            label: 'Status'
+            label: Status
           conditions:
             type: ignore
             label: 'Conditional logic'
           weight:
             type: integer
-            label: 'Weight'
+            label: Weight
           settings:
-            type: webform.handler.[%parent.id]
+            type: 'webform.handler.[%parent.id]'
     third_party_settings:
       type: sequence
       label: 'Third party settings'
       sequence:
-        type: webform.settings.third_party.[%key]
+        type: 'webform.settings.third_party.[%key]'
diff --git a/web/modules/webform/config/schema/webform.entity.webform_options.schema.yml b/web/modules/webform/config/schema/webform.entity.webform_options.schema.yml
index cee99cc44ae9908dc8e39019bb9494ecfc8be360..9973faf295214eec802e9fb2dcf88ef3627dfab3 100644
--- a/web/modules/webform/config/schema/webform.entity.webform_options.schema.yml
+++ b/web/modules/webform/config/schema/webform.entity.webform_options.schema.yml
@@ -1,16 +1,16 @@
-webform.webform_options.*:
+'webform.webform_options.*':
   type: config_entity
-  label: 'Options'
+  label: Options
   mapping:
     id:
       type: string
       label: 'Machine name'
     label:
       type: label
-      label: 'Label'
+      label: Label
     category:
       type: label
-      label: 'Category'
+      label: Category
     likert:
       type: boolean
       label: 'Use as Likert'
diff --git a/web/modules/webform/config/schema/webform.field.schema.yml b/web/modules/webform/config/schema/webform.field.schema.yml
index caa1d1558d33294a3c22bfc84e334728f8623c67..14582b2600c8c5fa6ce147ea1dffddef27d16175 100644
--- a/web/modules/webform/config/schema/webform.field.schema.yml
+++ b/web/modules/webform/config/schema/webform.field.schema.yml
@@ -1,3 +1,4 @@
+# @see field.storage_settings.entity_reference
 field.storage_settings.webform:
   type: mapping
   label: 'Webform field storage settings'
@@ -6,22 +7,25 @@ field.storage_settings.webform:
       type: string
       label: 'Type of item to reference'
 
-base_webform_field_field_settings:
+# @see field.field_settings.entity_reference
+field.field_settings.webform:
   type: mapping
+  label: 'Entity reference field settings'
   mapping:
     handler:
       type: string
       label: 'Reference method'
     handler_settings:
       type: entity_reference_selection.[%parent.handler]
-      label: 'Entity reference selection settings'
+      label: 'Entity reference selection plugin settings'
 
-field.field_settings.webform:
-  type: base_webform_field_field_settings
-  label: 'Webform settings'
+# @see field.value.entity_reference
+field.value.webform:
+  type: field.value.entity_reference
+  label: 'Default value'
   mapping:
     default_data:
-      type: string
+      type: text
       label: 'Default webform submission data'
     status:
       type: string
@@ -45,7 +49,7 @@ field.widget.settings.webform_entity_reference_autocomplete:
       label: 'Size of textfield'
     placeholder:
       type: label
-      label: 'Placeholder'
+      label: Placeholder
     default_data:
       type: boolean
       label: 'Default submission data'
@@ -57,7 +61,6 @@ field.widget.settings.webform_entity_reference_select:
     default_data:
       type: boolean
       label: 'Default submission data'
-
 field.formatter.settings.webform_entity_reference_entity_view:
   type: mapping
   label: 'Display the referenced webform with default submission data.'
@@ -73,3 +76,9 @@ field.formatter.settings.webform_entity_reference_link:
     label:
       type: label
       label: 'Link label to the referenced webform'
+    dialog:
+      type: string
+      label: 'Open referenced webform in modal dialog'
+    attributes:
+      type: ignore
+      label: 'Link attributes'
diff --git a/web/modules/webform/config/schema/webform.plugin.exporter.schema.yml b/web/modules/webform/config/schema/webform.plugin.exporter.schema.yml
index 742724c9ad5b5eade70fe5a980178fff4834bd11..161d852debfa55420f181d2a0cda9da623e9e781 100644
--- a/web/modules/webform/config/schema/webform.plugin.exporter.schema.yml
+++ b/web/modules/webform/config/schema/webform.plugin.exporter.schema.yml
@@ -1,11 +1,13 @@
-webform.exporter.*:
+'webform.exporter.*':
   type: mapping
   label: 'Exporter settings'
-
 webform.exporter.delimited_text:
   type: mapping
-  label: 'Delimiter'
+  label: Delimiter
   mapping:
     delimiter:
       type: string
-      label: 'Delimiter'
+      label: Delimiter
+    excel:
+      type: boolean
+      label: Excel
diff --git a/web/modules/webform/config/schema/webform.plugin.handler.schema.yml b/web/modules/webform/config/schema/webform.plugin.handler.schema.yml
index efa45561892440f682f6d769bf5bc3c2bbd651dd..35130ebda7b13940f56393353ead1b17fd339e2c 100644
--- a/web/modules/webform/config/schema/webform.plugin.handler.schema.yml
+++ b/web/modules/webform/config/schema/webform.plugin.handler.schema.yml
@@ -1,31 +1,30 @@
-webform.handler.*:
+'webform.handler.*':
   type: mapping
   label: 'Handler settings'
-
 webform.handler.action:
   type: mapping
-  label: 'Action'
+  label: Action
   mapping:
     states:
       type: sequence
-      label: 'States'
+      label: States
       sequence:
         type: string
-        label: 'State'
+        label: State
     notes:
-      label: 'Notes'
+      label: Notes
       type: text
     sticky:
-      label: 'Flag'
+      label: Flag
       type: boolean
     locked:
-      label: 'Locked'
+      label: Locked
       type: boolean
     data:
-      label: 'Data'
+      label: Data
       type: text
     message:
-      label: 'Message'
+      label: Message
       type: text
     message_type:
       label: 'Message type'
@@ -33,22 +32,20 @@ webform.handler.action:
     debug:
       type: boolean
       label: 'Enable debugging'
-
 webform.handler.log:
   type: mapping
-  label: 'Log'
+  label: Log
   mapping: {  }
-
 webform.handler.email:
   type: mapping
-  label: 'Email'
+  label: Email
   mapping:
     states:
       type: sequence
-      label: 'States'
+      label: States
       sequence:
         type: string
-        label: 'State'
+        label: State
     to_mail:
       label: 'Email to address'
       type: email
@@ -105,29 +102,34 @@ webform.handler.email:
       label: 'Always include private and restricted access elements.'
     exclude_empty:
       type: boolean
-      label: 'Preview exclude empty elements'
+      label: 'Exclude empty elements'
+    exclude_empty_checkbox:
+      type: boolean
+      label: 'Exclude unselected checkboxes'
     html:
       type: boolean
-      label: 'HTML'
+      label: HTML
     attachments:
       type: boolean
-      label: 'Attachments'
+      label: Attachments
     twig:
       type: boolean
-      label: 'Twig'
+      label: Twig
+    theme_name:
+      type: string
+      label: 'Theme name'
     debug:
       type: boolean
       label: 'Enable debugging'
-
 webform.handler.remote_post:
   type: mapping
   label: 'Remote Post'
   mapping:
     method:
-      label: 'Method'
+      label: Method
       type: string
     type:
-      label: 'Type'
+      label: Type
       type: string
     excluded_data:
       type: sequence
@@ -182,7 +184,7 @@ webform.handler.remote_post:
       type: sequence
       sequence:
         type: mapping
-        label: 'Message'
+        label: Message
         mapping:
           code:
             type: integer
@@ -190,18 +192,13 @@ webform.handler.remote_post:
           message:
             type: text
             label: 'Response message'
-
 webform.handler.settings:
   type: mapping
-  label: 'Settings'
+  label: Settings
   mapping:
     debug:
       type: string
       label: 'Enable debugging'
-    # Below mapping is copied from: webform.webform.*
-    # @see webform.entity.webform.schema.yml
-    # NOTE: 'type: string' is changed to 'type: string
-    # NOTE: 'type: string' is changed to 'type: string
     ajax:
       type: string
       label: 'Use Ajax'
@@ -276,19 +273,25 @@ webform.handler.settings:
       label: 'Display required indicator'
     form_autofocus:
       type: string
-      label: 'Autofocus'
+      label: Autofocus
     form_details_toggle:
       type: string
       label: 'Display collapse/expand all details link'
     form_reset:
       type: string
       label: 'Display reset button'
-    form_login:
+    form_access_denied:
       type: string
-      label: 'Redirect to login when access denied to webform'
-    form_login_message:
+      label: 'Form access denied action'
+    form_access_denied_title:
+      type: label
+      label: 'Form access denied title'
+    form_access_denied_message:
       type: text
-      label: 'Login message when access denied to webform'
+      label: 'Form access denied message'
+    form_access_denied_attributes:
+      type: ignore
+      label: 'Form access denied message attributes'
     submission_label:
       type: label
       label: 'Default submission label'
@@ -301,18 +304,80 @@ webform.handler.settings:
     submission_log:
       type: string
       label: 'Submission logging'
+    submission_views:
+      type: sequence
+      label: 'Submission views'
+      sequence:
+        type: mapping
+        label: 'Submission view'
+        mapping:
+          title:
+            type: text
+            label: Title
+          view:
+            type: string
+            label: 'View name / Display ID'
+          webform_routes:
+            type: sequence
+            label: 'Apply to webform'
+            sequence:
+              type: string
+              label: Route
+          node_routes:
+            type: sequence
+            label: 'Apply to node'
+            sequence:
+              type: string
+              label: Route
+    submission_views_replace:
+      type: mapping
+      label: 'Submission view replace'
+      mapping:
+        global_routes:
+          type: sequence
+          label: 'Replace to global'
+          sequence:
+            type: string
+            label: Route
+        webform_routes:
+          type: sequence
+          label: 'Replace to webform'
+          sequence:
+            type: string
+            label: Route
+        node_routes:
+          type: sequence
+          label: 'Replace to node'
+          sequence:
+            type: string
+            label: Route
     submission_user_columns:
       type: sequence
       label: 'Submission user columns'
       sequence:
         type: string
         label: 'Column name'
-    submission_login:
+    submission_user_duplicate:
+      type: string
+      label: 'Submission user duplicate'
+    submission_access_denied:
       type: string
-      label: 'Redirect to login when access denied to submission'
-    submission_login_message:
+      label: 'Submission access denied action'
+    submission_access_denied_title:
+      type: label
+      label: 'Submission access denied title'
+    submission_access_denied_message:
+      type: text
+      label: 'Submission access denied message'
+    submission_access_denied_attributes:
+      type: ignore
+      label: 'Submission access denied message attributes'
+    previous_submission_message:
       type: text
-      label: 'Login message when access denied to submission'
+      label: 'Previous submission message'
+    previous_submissions_message:
+      type: text
+      label: 'Previous submissions message'
     autofill:
       type: string
       label: 'Autofill with previous submission'
@@ -334,12 +399,18 @@ webform.handler.settings:
     wizard_progress_percentage:
       type: string
       label: 'Show wizard progress pages'
+    wizard_progress_link:
+      type: string
+      label: 'Link to previous pages in progress bar'
     wizard_start_label:
       type: label
       label: 'Wizard start label'
     wizard_start_attributes:
       type: ignore
       label: 'Wizard start attributes'
+    wizard_preview_link:
+      type: string
+      label: 'Link to previous pages in preview'
     wizard_confirmation:
       type: string
       label: 'Include confirmation page in progress'
@@ -373,6 +444,9 @@ webform.handler.settings:
     preview_exclude_empty:
       type: string
       label: 'Preview exclude empty elements'
+    preview_exclude_empty_checkbox:
+      type: string
+      label: 'Preview exclude unselected checkboxes'
     draft:
       type: string
       label: 'Allow users to save and finish the webform later.'
@@ -395,7 +469,7 @@ webform.handler.settings:
       type: string
       label: 'Confirmation URL'
     confirmation_title:
-      type: text
+      type: label
       label: 'Confirmation title'
     confirmation_message:
       type: text
@@ -427,6 +501,9 @@ webform.handler.settings:
     limit_total_message:
       type: text
       label: 'Limit total message'
+    limit_total_unique:
+      type: string
+      label: 'Limit total to one submission per source entity'
     limit_user:
       type: string
       label: 'Limit user submissions'
@@ -436,6 +513,9 @@ webform.handler.settings:
     limit_user_message:
       type: text
       label: 'Limit user message'
+    limit_user_unique:
+      type: string
+      label: 'Limit user to one submission per source entity'
     entity_limit_total:
       type: string
       label: 'Entity limit total submissions'
diff --git a/web/modules/webform/config/schema/webform.settings.schema.yml b/web/modules/webform/config/schema/webform.settings.schema.yml
index d29a2e742947eb4228977f4b6d41e5a196d93f41..546b0f583354f07a1fbe3f2b448e97029387ff7f 100644
--- a/web/modules/webform/config/schema/webform.settings.schema.yml
+++ b/web/modules/webform/config/schema/webform.settings.schema.yml
@@ -6,33 +6,36 @@ webform.settings:
       type: mapping
       label: 'Webform default settings'
       mapping:
+        default_status:
+          type: string
+          label: 'Default status'
         default_page_base_path:
           type: string
           label: 'Default base path'
         default_submit_button_label:
           type: label
-          label: 'Default webform submit button label'
+          label: 'Default submit button label'
         default_reset_button_label:
           type: label
-          label: 'Default webform reset button label'
+          label: 'Default reset button label'
         default_form_submit_once:
           type: boolean
           label: 'Prevent duplicate submissions'
         default_form_open_message:
           type: text
-          label: 'Default webform open message'
+          label: 'Default form open message'
         default_form_close_message:
           type: text
-          label: 'Default webform close message'
+          label: 'Default form close message'
         default_form_exception_message:
           type: text
-          label: 'Default webform exception message'
+          label: 'Default form exception message'
         default_form_confidential_message:
           type: text
-          label: 'Default webform confidential message'
-        default_form_login_message:
+          label: 'Default form confidential message'
+        default_form_access_denied_message:
           type: text
-          label: 'Default login message when access denied to webform'
+          label: 'Default form access denied message'
         default_form_novalidate:
           type: boolean
           label: 'Disable client-side validation'
@@ -57,6 +60,9 @@ webform.settings:
         default_form_details_toggle:
           type: boolean
           label: 'Display collapse/expand all details link'
+        default_form_file_limit:
+          type: string
+          label: 'Default file upload limit per form'
         default_wizard_prev_button_label:
           type: label
           label: 'Default wizard previous page button label'
@@ -111,15 +117,74 @@ webform.settings:
         default_submission_log:
           type: boolean
           label: 'Default submission logging'
-        default_submission_login_message:
+        default_submission_views:
+          type: sequence
+          label: 'Default submission views'
+          sequence:
+            type: mapping
+            label: 'Submission view'
+            mapping:
+              title:
+                type: text
+                label: Title
+              view:
+                type: string
+                label: 'View name / Display ID'
+              global_routes:
+                type: sequence
+                label: 'Apply to global'
+                sequence:
+                  type: string
+                  label: Route
+              webform_routes:
+                type: sequence
+                label: 'Apply to webform'
+                sequence:
+                  type: string
+                  label: Route
+              node_routes:
+                type: sequence
+                label: 'Apply to node'
+                sequence:
+                  type: string
+                  label: Route
+        default_submission_views_replace:
+          type: mapping
+          label: 'Default submission view replace'
+          mapping:
+            global_routes:
+              type: sequence
+              label: 'Replace to global'
+              sequence:
+                type: string
+                label: Route
+            webform_routes:
+              type: sequence
+              label: 'Replace to webform'
+              sequence:
+                type: string
+                label: Route
+            node_routes:
+              type: sequence
+              label: 'Replace to node'
+              sequence:
+                type: string
+                label: Route
+        default_submission_access_denied_message:
           type: text
-          label: 'Default login message when access denied to submission'
+          label: 'Default submission access denied message'
         default_submission_exception_message:
           type: text
           label: 'Default submission exception message'
         default_submission_locked_message:
           type: text
           label: 'Default submission locked message'
+        default_previous_submission_message:
+          type: text
+          label: 'Default previous submission message'
+        default_previous_submissions_message:
+          type: text
+          label: 'Default previous submissions message'
         default_autofill_message:
           type: text
           label: 'Default submission autofill message'
@@ -138,6 +203,28 @@ webform.settings:
         confirmation_back_classes:
           type: string
           label: 'Confirmation back link CSS classes'
+        dialog:
+          type: boolean
+          label: 'Enable webform dialog support'
+        dialog_options:
+          type: sequence
+          label: 'Preset dialog options'
+          sequence:
+            type: mapping
+            label: 'Dialog options'
+            mapping:
+              name:
+                type: string
+                label: 'Dialog name'
+              title:
+                type: label
+                label: 'Dialog title'
+              width:
+                type: integer
+                label: 'Dialog width'
+              height:
+                type: integer
+                label: 'Dialog height'
     assets:
       type: mapping
       label: 'Global Assets (CSS/JavaScript)'
@@ -147,7 +234,7 @@ webform.settings:
           label: 'CSS (Cascading Style Sheets)'
         javascript:
           type: string
-          label: 'JavaScript'
+          label: JavaScript
     element:
       type: mapping
       label: 'Element default settings'
@@ -191,6 +278,12 @@ webform.settings:
         default_google_maps_api_key:
           type: string
           label: 'Default Google Maps API key'
+        default_algolia_places_app_id:
+          type: string
+          label: 'Default Algolia application id'
+        default_algolia_places_api_key:
+          type: string
+          label: 'Default Algolia API key'
         excluded_elements:
           type: ignore
           label: 'Excluded types'
@@ -201,12 +294,18 @@ webform.settings:
         disabled:
           type: boolean
           label: 'Disable HTML editor'
-        format:
+        element_format:
           type: string
-          label: 'Text format'
+          label: 'Element text format'
+        mail_format:
+          type: string
+          label: 'Mail text format'
         tidy:
           type: boolean
           label: 'Tidy HTML markup'
+        make_unused_managed_files_temporary:
+          type: boolean
+          label: 'Controls if unused HTML editor files should be marked temporary'
     file:
       type: mapping
       label: 'File upload default settings'
@@ -222,7 +321,7 @@ webform.settings:
           label: 'Login message when access denied to private file uploads.'
         default_max_filesize:
           type: string
-          label: 'Default maximum upload size'
+          label: 'Default maximum file upload size'
         default_managed_file_extensions:
           type: string
           label: 'Default allowed managed file extensions'
@@ -240,10 +339,10 @@ webform.settings:
           label: 'Default allowed document file extensions'
         make_unused_managed_files_temporary:
           type: boolean
-          label: 'Controls if unused files should be marked temporary'
+          label: 'Controls if unused webform submission files should be marked temporary'
         delete_temporary_managed_files:
           type: boolean
-          label: 'Immediately deleted temporary managed files'
+          label: 'Immediately deleted temporary managed webform submission files'
     format:
       type: ignore
       label: 'Format default settings'
@@ -283,14 +382,17 @@ webform.settings:
           label: 'Default email body (HTML)'
         roles:
           type: sequence
-          label: 'Roles'
+          label: Roles
           sequence:
             type: string
-            label: 'Role'
+            label: Role
     export:
       type: mapping
       label: 'Export default settings'
       mapping:
+        temp_directory:
+          type: string
+          label: 'Export temporary directory'
         exporter:
           type: string
           label: 'Results exporter'
@@ -359,6 +461,9 @@ webform.settings:
         default_batch_export_size:
           type: integer
           label: 'Batch export size'
+        default_batch_import_size:
+          type: integer
+          label: 'Batch import size'
         default_batch_update_size:
           type: integer
           label: 'Batch update size'
@@ -392,6 +497,9 @@ webform.settings:
         video_display:
           type: string
           label: 'Video display'
+        help_disabled:
+          type: boolean
+          label: 'Disable help'
         dialog_disabled:
           type: boolean
           label: 'Disable dialogs'
@@ -422,7 +530,7 @@ webform.settings:
           label: 'Use CDN'
     requirements:
       type: mapping
-      label: 'Requirements'
+      label: Requirements
       mapping:
         cdn:
           type: boolean
@@ -435,7 +543,7 @@ webform.settings:
           label: 'Check if SPAM protection module is installed'
     contribute:
       type: mapping
-      label: 'Contribute'
+      label: Contribute
       mapping:
         account_type:
           type: string
@@ -447,4 +555,4 @@ webform.settings:
       type: sequence
       label: 'Third party settings'
       sequence:
-        type: webform.admin_settings.third_party.[%key]
+        type: 'webform.admin_settings.third_party.[%key]'
diff --git a/web/modules/webform/config/schema/webform.third_party.captcha.schema.yml b/web/modules/webform/config/schema/webform.third_party.captcha.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fbbc4c2ada6c05105b94f8be0dc77e22789c88d6
--- /dev/null
+++ b/web/modules/webform/config/schema/webform.third_party.captcha.schema.yml
@@ -0,0 +1,7 @@
+webform.admin_settings.third_party.captcha:
+  type: mapping
+  label: 'CAPTCHA third party settings'
+  mapping:
+    replace_administration_mode:
+      type: boolean
+      label: 'Replace ''Add CAPTCHA administration links to forms'' with CAPTCHA webform element'
diff --git a/web/modules/webform/config/schema/webform.third_party.honeypot.schema.yml b/web/modules/webform/config/schema/webform.third_party.honeypot.schema.yml
index a85d6cddece80368f89d062393e23f72deb0f27a..21e4469c7849ce3611c3790ec5b36a4741241749 100644
--- a/web/modules/webform/config/schema/webform.third_party.honeypot.schema.yml
+++ b/web/modules/webform/config/schema/webform.third_party.honeypot.schema.yml
@@ -1,6 +1,6 @@
 webform.admin_settings.third_party.honeypot:
   type: mapping
-  label: 'Webform test third party settings'
+  label: 'Honeypot third party settings'
   mapping:
     honeypot:
       type: boolean
@@ -8,7 +8,6 @@ webform.admin_settings.third_party.honeypot:
     time_restriction:
       type: boolean
       label: 'Add time limit to all webforms'
-
 webform.settings.third_party.honeypot:
   type: mapping
   label: 'Webform test third party settings'
diff --git a/web/modules/webform/css/webform.addons.css b/web/modules/webform/css/webform.addons.css
index 25284d472b9afd2c8f3e62e4a8157de0f2b7b131..6d79fb45deacb19891306f06199022693bcb30ed 100644
--- a/web/modules/webform/css/webform.addons.css
+++ b/web/modules/webform/css/webform.addons.css
@@ -3,6 +3,11 @@
  * Addons styles
  */
 
+.webform-addons-summary {
+  float: left;
+  margin: 0 0 0.5em 0;
+}
+
 /**
  * Projects.
  */
diff --git a/web/modules/webform/css/webform.admin.composite.css b/web/modules/webform/css/webform.admin.composite.css
index 018b22903dac037a27fc02f0f2a2d6ee26d560c2..a4af14e8eccac79d028e8ea78ad67479bc68367e 100644
--- a/web/modules/webform/css/webform.admin.composite.css
+++ b/web/modules/webform/css/webform.admin.composite.css
@@ -13,6 +13,6 @@
   width: 100%;
 }
 
-.webform-admin-composite-elements table .form-type-checkbox {
+.webform-admin-composite-elements table .form-type-checkbox.form-no-label {
   text-align: center;
 }
diff --git a/web/modules/webform/css/webform.admin.css b/web/modules/webform/css/webform.admin.css
index 82b71767675dd73c4ba7f65979e8c59437274415..b32a6bf89ad455bbe3a7f9541580189fdfa220ad 100644
--- a/web/modules/webform/css/webform.admin.css
+++ b/web/modules/webform/css/webform.admin.css
@@ -10,6 +10,14 @@
   margin-bottom: 0;
 }
 
+/**
+ * Submission.
+ */
+
+.webform-submission-information .button {
+  font-size: 1em;
+}
+
 /**
  * Webform setting button displays a gear icon.
  * @see /admin/structure/webform/manage/contact/results/submissions
@@ -23,34 +31,13 @@
   font-size: 1.2em;
 }
 
-/**
- * Filter webform and submissions.
- * @see /admin/structure/webform
- * @see /admin/structure/webform/templates
- */
-.webform-filter-form .form-submit {
-  margin-left: 0;
-  margin-right: 5px;
-}
-
-.webform-filter-form .form-item {
-  margin-right: 5px;
-}
-
-@media screen and (max-width: 600px) {
-  .webform-filter-form .form-item {
-    display: block;
-    margin-right: 0;
-  }
-}
-
 /**
  * Results table.
  * @see /admin/structure/webform/submissions/manage
  * @see /admin/structure/webform/manage/contact/results/submissions
  */
-.webform-results__table th,
-.webform-results__table td {
+.webform-results-table th,
+.webform-results-table td {
   max-width: 400px;
   overflow: hidden;
   white-space: nowrap;
@@ -58,44 +45,35 @@
 }
 
 /* Make sure 'Operations' is never cut-off. */
-.webform-results__table td.webform-dropbutton-wrapper {
+.webform-results-table td.webform-dropbutton-wrapper {
   overflow: visible;
   max-width: inherit;
   white-space: normal;
 }
 
-th.webform-results__icon,
-td.webform-results__icon {
+th.webform-results-table__icon,
+td.webform-results-table__icon {
   padding-left: 0;
   padding-right: 0;
 }
 
 /* Hide throbber, which breaks icon alignment */
-.webform-results__icon .ajax-progress-throbber,
-a.webform-results__custom + .ajax-progress-throbber {
+.webform-results-table__icon .ajax-progress-throbber {
   display: none;
 }
 
 /* Entire row is clickable. @see Drupal.behaviors.webformTableRowHref */
-.webform-results__table tbody tr {
+.webform-results-table tbody tr {
   cursor: pointer;
   cursor: hand;
 }
 
 /* Limit image height to 60px */
-.webform-results__table td img {
+.webform-results-table td img {
   max-height: 60px;
   width: auto;
 }
 
-/**
- * Results custom(ize) dialog.
- */
-.webform-results__custom .tabledrag-changed-warning,
-.webform-results__custom .tabledrag-changed /* Hide table drag warnings. */ {
-  display: none !important; /* Must use !important because .tabledrag-changed 'display' is set via JavaScript */
-}
-
 /**
  * Results icons (notes, sticky, and locked)
  */
@@ -184,15 +162,37 @@ a:focus .webform-icon-locked--off {
  * Submission view table.
  * @see /admin/structure/webform/manage/{webform_id}/submission/{webform_submission_id}/table
  */
-.webform-submission__table th {
+.webform-submission-table th {
   width: 33%;
   min-width: 100px;
 }
 
-.webform-submission__table td {
+.webform-submission-table td {
   width: 66%;
 }
 
 .webform-horizontal-rule {
   margin: .5em 0;
 }
+
+/**
+ * Handlers table.
+ * @see /admin/structure/webform/manage/{webform_id}/handlers
+ */
+.webform-handlers-table .ajax-progress-throbber {
+  display: none;
+}
+
+@media screen and (max-width: 1280px) {
+  .js-off-canvas-dialog-open .webform-handlers-table th,
+  .js-off-canvas-dialog-open .webform-handlers-table td {
+    display: none;
+  }
+
+  .js-off-canvas-dialog-open .webform-handlers-table th:first-child,
+  .js-off-canvas-dialog-open .webform-handlers-table th:last-child,
+  .js-off-canvas-dialog-open .webform-handlers-table td:first-child,
+  .js-off-canvas-dialog-open .webform-handlers-table td:last-child {
+    display: table-cell;
+  }
+}
diff --git a/web/modules/webform/css/webform.admin.dropbutton.css b/web/modules/webform/css/webform.admin.dropbutton.css
index 9f76a15a63726e8b4d49db3c7135735ea278ae72..e7eb6cbc9d4359edd80fa8e65a6d508b6ff73b2f 100644
--- a/web/modules/webform/css/webform.admin.dropbutton.css
+++ b/web/modules/webform/css/webform.admin.dropbutton.css
@@ -8,6 +8,19 @@
  * @see /admin/structure/webform/manage/{webform}/handlers
  */
 
+/**
+ * Fix webform submission views dropdown display.
+ */
+.js .webform-submission-views-dropbutton .dropbutton-wrapper {
+  display: block;
+  position: relative;
+  min-height: 2em;
+}
+
+.js .webform-submission-views-dropbutton .dropbutton-widget {
+  position: absolute;
+}
+
 /**
  * Hide dropbutton to prevent FOUC (flash of unstyled content).
  */
diff --git a/web/modules/webform/css/webform.admin.settings.css b/web/modules/webform/css/webform.admin.settings.css
new file mode 100644
index 0000000000000000000000000000000000000000..4af9a04066e2675d3ac76b0dc5b044445c114fb1
--- /dev/null
+++ b/web/modules/webform/css/webform.admin.settings.css
@@ -0,0 +1,13 @@
+/**
+ * @file
+ * Admin settings styles
+ */
+
+/**
+ * Dialog test button.
+ * @see /admin/structure/webform/manage/{webform}/settings
+ */
+.webform-settings-form .webform-dialog.button {
+  width: 100%;
+  white-space: nowrap;
+}
diff --git a/web/modules/webform/css/webform.admin.tabledrag.css b/web/modules/webform/css/webform.admin.tabledrag.css
new file mode 100644
index 0000000000000000000000000000000000000000..ce21249c767d166778cade8bb42591d63c99e498
--- /dev/null
+++ b/web/modules/webform/css/webform.admin.tabledrag.css
@@ -0,0 +1,29 @@
+/**
+ * @file
+ * Tabledrag styles.
+ */
+
+.tabledrag-toggle-weight-wrapper {
+  position: relative;
+}
+
+.tabledrag-toggle-weight-wrapper button {
+  position: absolute;
+  top: -2.5em;
+  right: 0;
+}
+
+@media screen and (max-width: 960px) {
+  .tabledrag-toggle-weight-wrapper {
+    float: right;
+  }
+  .tabledrag-toggle-weight-wrapper button {
+    position: static;
+    top: inherit;
+    right: inherit;
+  }
+}
+
+.webform-tabledrag-hide {
+  display: none; !important;
+}
diff --git a/web/modules/webform/css/webform.ajax.css b/web/modules/webform/css/webform.ajax.css
index d612690be045865d5a6c3a589b58e69e2286bf48..9468e95d416f92e59861c30d6bd2ea62f4c7d90f 100644
--- a/web/modules/webform/css/webform.ajax.css
+++ b/web/modules/webform/css/webform.ajax.css
@@ -46,10 +46,10 @@
  * @see core/misc/dialog/dialog.position.js
  * @see \Drupal\webform\Utility\WebformDialogHelper::getModalDialogAttributes
  */
-.webform-modal {
+.webform-ui-dialog {
   top: 50px !important;
 }
 
-.toolbar-tray-open.toolbar-horizontal .webform-modal {
+.toolbar-tray-open.toolbar-horizontal .webform-ui-dialog {
   top: 90px !important;
 }
diff --git a/web/modules/webform/css/webform.composite.css b/web/modules/webform/css/webform.composite.css
index e545cb16c266ea1ebe260d018c1d0579f83312b0..550c2d5594d834aa6f5720a392188c2151ab2975 100644
--- a/web/modules/webform/css/webform.composite.css
+++ b/web/modules/webform/css/webform.composite.css
@@ -6,6 +6,19 @@
 /**
  * Remove extra margin are composite element which already contain form elements with margins
  */
-.form-composite {
-  margin: 0;
+fieldset.webform-composite-hidden-title {
+  margin-top: 1em;
+  margin-bottom: 1em;
+}
+
+fieldset.webform-composite-hidden-title .fieldset-wrapper > .form-item:first-child,
+fieldset.webform-composite-hidden-title .fieldset-wrapper > .form-wrapper > .form-item:first-child,
+fieldset.webform-composite-hidden-title .fieldset-wrapper > .webform-flexbox:first-child {
+  margin-top: 0;
+}
+
+fieldset.webform-composite-hidden-title .fieldset-wrapper > .form-item:last-child,
+fieldset.webform-composite-hidden-title .fieldset-wrapper > .form-wrapper > .form-item:last-child,
+fieldset.webform-composite-hidden-title .fieldset-wrapper > .webform-flexbox:last-child {
+  margin-bottom: 0;
 }
diff --git a/web/modules/webform/css/webform.element.checkbox_value.css b/web/modules/webform/css/webform.element.checkbox_value.css
index 2c1edd77238096d5f5b12b96e3f988e73648301f..5067fbddb75996f41e1c5f93884bd5a60317efb7 100644
--- a/web/modules/webform/css/webform.element.checkbox_value.css
+++ b/web/modules/webform/css/webform.element.checkbox_value.css
@@ -6,5 +6,5 @@
  */
 
 .form-item.form-type-webform-checkbox-value {
-  margin: 0
+  margin: 0;
 }
diff --git a/web/modules/webform/css/webform.element.composite.css b/web/modules/webform/css/webform.element.composite.css
index d80dda63ae5317fd7b8ec25a6051dbf090cb5bf8..d1147f9f6b23a7d774f0ac7598a607138a4515bd 100644
--- a/web/modules/webform/css/webform.element.composite.css
+++ b/web/modules/webform/css/webform.element.composite.css
@@ -13,7 +13,7 @@
   width: 100%;
 }
 
-.form-type-webform-element-composite table .form-type-checkbox {
+.form-type-webform-element-composite table .form-type-checkbox.form-no-label {
   text-align: center;
 }
 
diff --git a/web/modules/webform/css/webform.element.computed.css b/web/modules/webform/css/webform.element.computed.css
new file mode 100644
index 0000000000000000000000000000000000000000..8979b4105fef41cf2321bcd9ab6037f5fad476b6
--- /dev/null
+++ b/web/modules/webform/css/webform.element.computed.css
@@ -0,0 +1,8 @@
+/**
+ * @file
+ * Element computed styles.
+ */
+
+.webform-computed-loading {
+  background-color: #ffffaa;
+}
diff --git a/web/modules/webform/css/webform.element.counter.css b/web/modules/webform/css/webform.element.counter.css
new file mode 100644
index 0000000000000000000000000000000000000000..684e64d65469e75270e5819ae910d8f54fe097cd
--- /dev/null
+++ b/web/modules/webform/css/webform.element.counter.css
@@ -0,0 +1,17 @@
+/**
+ * @file
+ * Element counter styles.
+ */
+
+input.webform-counter-warning,
+input.form-text.webform-counter-warning,
+textarea.webform-counter-warning,
+textarea.form-textarea.webform-counter-warning {
+  border-color: #e09600;
+  background: #fdf8ed;
+}
+
+.text-count-wrapper.webform-counter-warning {
+  color: #734c00;
+  font-weight: bold;
+}
diff --git a/web/modules/webform/css/webform.element.date.css b/web/modules/webform/css/webform.element.date.css
new file mode 100644
index 0000000000000000000000000000000000000000..f51802f1d28fabdf11fe4e91370fed88f9273b53
--- /dev/null
+++ b/web/modules/webform/css/webform.element.date.css
@@ -0,0 +1,8 @@
+/**
+ * @file
+ * Element date styles.
+ */
+
+.ui-datepicker-trigger {
+  margin: 0 2px;
+}
diff --git a/web/modules/webform/css/webform.element.datelist.css b/web/modules/webform/css/webform.element.datelist.css
new file mode 100644
index 0000000000000000000000000000000000000000..8381b151a57bf17be5336189647d039e4e70f9c0
--- /dev/null
+++ b/web/modules/webform/css/webform.element.datelist.css
@@ -0,0 +1,8 @@
+/**
+ * @file
+ * Element datelist styles.
+ */
+
+.form-type-datelist input[type="text"] {
+  max-width: 5em;
+}
diff --git a/web/modules/webform/css/webform.element.details.toggle.css b/web/modules/webform/css/webform.element.details.toggle.css
index f980233eb81de9c8a18a0b1e1e5523788be6f6d7..d7fbccc79cae59d047b52d6d6621b7916cf88d45 100644
--- a/web/modules/webform/css/webform.element.details.toggle.css
+++ b/web/modules/webform/css/webform.element.details.toggle.css
@@ -19,3 +19,34 @@
 .webform-details-toggle-state-wrapper + details {
   margin-top: 0;
 }
+
+.webform-details-toggle-state {
+  background: transparent;
+  border: 0;
+  cursor: pointer;
+  margin-top: 0;
+  padding: 0;
+  font-size: 1em;
+}
+
+/* Tweak details toggle state. */
+.webform-details-toggle-state {
+  color: #337ab7;
+  text-decoration: none;
+}
+
+.webform-details-toggle-state:hover,
+.webform-details-toggle-statelink:focus {
+  text-decoration: underline;
+}
+
+/* Float toogle to the right of webform tabs */
+.webform-tabs .webform-details-toggle-state-wrapper {
+  float: right;
+}
+
+@media screen and (max-width: 600px) {
+  .webform-tabs .webform-details-toggle-state-wrapper {
+    float: none;
+  }
+}
diff --git a/web/modules/webform/css/webform.element.help.css b/web/modules/webform/css/webform.element.help.css
index 9bf6d433be7b8b3041d7a5fd91522cde3f140c18..79f01f6e0649d6de26afadd5c67380313a61f649 100644
--- a/web/modules/webform/css/webform.element.help.css
+++ b/web/modules/webform/css/webform.element.help.css
@@ -5,11 +5,14 @@
 
 .webform-element-help {
   display: inline-block;
+  box-sizing: content-box;
   border: 2px solid #bbb;
   background: #bbb;
   line-height: 14px;
+  height: 14px;
   width: 14px;
   font-size: 12px;
+  color: #fff;
   border-radius: 50%;
   font-weight: bold;
   text-align: center;
@@ -17,22 +20,27 @@
   margin: 0 .3em;
 }
 
-.webform-element-help:link,
-.webform-element-help:visited {
-  color: #fff;
-  text-decoration: none;
-}
-
 .webform-element-help:focus,
 .webform-element-help:active,
 .webform-element-help:hover {
   border: 2px solid #0074bd;
   background: #0074bd;
   color: #fff;
-  text-decoration: none;
   cursor: help;
 }
 
+.webform-element-help--title {
+  font-weight: bold;
+  font-size: 1.1em;
+  margin: 0 0 .2em 0;
+}
+
 .ui-tooltip.webform-element-help--tooltip {
   max-width: 400px;
 }
+
+@media only screen and (max-width : 400px) {
+  .ui-tooltip.webform-element-help--tooltip {
+    max-width: 300px;
+  }
+}
diff --git a/web/modules/webform/css/webform.element.image_file.css b/web/modules/webform/css/webform.element.image_file.css
index 99bd1abbc1e0215b80b1cff2e24c5fb8bb88c7c1..fcbe0d2f0ecdcb40f4dcd55de88d22dc15b5221f 100644
--- a/web/modules/webform/css/webform.element.image_file.css
+++ b/web/modules/webform/css/webform.element.image_file.css
@@ -9,3 +9,7 @@ img.webform-image-file {
   max-width: 100%;
   height: auto;
 }
+
+.webform-image-file-modal img {
+  display: block;
+}
diff --git a/web/modules/webform/css/webform.element.likert.css b/web/modules/webform/css/webform.element.likert.css
index 4043e8047f21f194a287c8e565a1d8a3a5b002e0..b324af7ff1e1cfcf46f7a8b289dec7957d14c9ae 100644
--- a/web/modules/webform/css/webform.element.likert.css
+++ b/web/modules/webform/css/webform.element.likert.css
@@ -39,28 +39,18 @@
 /**
  * Basic table formatting.
  */
-.webform-likert-table th,
-.webform-likert-table td {
+.webform-likert-table-wrapper th,
+.webform-likert-table-wrapper td {
   text-align: center;
 }
 
-/**
- * Hide radio title and description when Likert is displayed within a
- * grid on desktop.
- * @see \Drupal\webform\Element\WebformLikert::processWebformLikert
- */
-.webform-likert-label,
-.webform-likert-description {
-  display: none;
-}
-
-.webform-likert-table th:first-child,
-.webform-likert-table td:first-child {
+.webform-likert-table-wrapper th:first-child,
+.webform-likert-table-wrapper td:first-child {
   text-align: inherit;
   width: 40%;
 }
 
-.form-type-webform-likert td:first-child label {
+.webform-likert-table-wrapper td:first-child label {
   display: block;
 }
 
@@ -69,6 +59,10 @@
  * grid to inline vertical radios.
  */
 @media (max-width: 768px) {
+  .form-type-webform-likert table.sticky-header {
+    display: none;
+  }
+
   .form-type-webform-likert table {
     border-collapse: collapse;
     font-size: inherit;
@@ -100,17 +94,31 @@
   }
 
   /**
-   * Show radio title and description when Likert is displayed inline on mobile.
+   * Show visually hidden radio title and description
+   * when Likert is displayed inline on mobile.
    * @see \Drupal\webform\Element\WebformLikert::processWebformLikert
+   * @see core/modules/system/css/components/hidden.module.css
    */
-  .webform-likert-label {
+  .webform-likert-table .visually-hidden {
+    position: inherit !important;
+    clip: inherit;
+    overflow: inherit;
+    height: inherit;
+    width: inherit;
+  }
+
+  .webform-likert-label.visually-hidden {
     display: inline;
   }
 
-  .webform-likert-description {
+  .webform-likert-description.hidden {
     display: block;
   }
 
+  .webform-likert-help.hidden {
+    display: inline;
+  }
+
   .form-type-webform-likert td:last-child {
     margin-bottom: 1em;
   }
diff --git a/web/modules/webform/css/webform.element.location.css b/web/modules/webform/css/webform.element.location.geocomplete.css
similarity index 72%
rename from web/modules/webform/css/webform.element.location.css
rename to web/modules/webform/css/webform.element.location.geocomplete.css
index c13ce73f0f91cbb54c71ab63b7c30ce64dbab2d7..e60b7efc65a6b4ad139abf5d48d1b9186194a853 100644
--- a/web/modules/webform/css/webform.element.location.css
+++ b/web/modules/webform/css/webform.element.location.geocomplete.css
@@ -1,14 +1,14 @@
 /**
  * @file
- * Location element styles.
+ * Location geocomplete element styles.
  *
- * @see /webform/test_element_location
+ * @see /webform/test_element_loc_geocomplete
  */
 
 /**
  * Map styles.
  */
-.webform-location-map {
+.webform-location-geocomplete-map {
   margin-top: 5px;
   border: 1px solid #ccc;
   width: 100%;
@@ -17,13 +17,13 @@
   position: relative;
 }
 
-.webform-location-map:after {
+.webform-location-geocomplete-map:after {
   padding-top: 56.25%; /* 16:9 ratio */
   display: block;
   content: '';
 }
 
-.webform-location-map--container {
+.webform-location-geocomplete-map--container {
   position: absolute;
   top: 0;
   bottom: 0;
diff --git a/web/modules/webform/css/webform.element.location.places.css b/web/modules/webform/css/webform.element.location.places.css
new file mode 100644
index 0000000000000000000000000000000000000000..51510b90b0dec6158128cc53c573a0e32d2f3d0e
--- /dev/null
+++ b/web/modules/webform/css/webform.element.location.places.css
@@ -0,0 +1,16 @@
+/**
+ * @file
+ * Location Algolia places element styles.
+ *
+ * @see https://community.algolia.com/places/documentation.html#css-classes
+ * @see /webform/test_element_loc_places
+ */
+
+.ap-input {
+  padding-left: 4px;
+  padding-top: 4px;
+  padding-bottom: 4px;
+  line-height: 2em;
+  height: 2em;
+  border-radius: unset;
+}
diff --git a/web/modules/webform/css/webform.element.managed_file.css b/web/modules/webform/css/webform.element.managed_file.css
new file mode 100644
index 0000000000000000000000000000000000000000..6e1e890deb9c25c7775701e32c7893a17161a015
--- /dev/null
+++ b/web/modules/webform/css/webform.element.managed_file.css
@@ -0,0 +1,30 @@
+/**
+ * @file
+ * Managed file styles.
+ */
+
+.webform-managed-file-preview-wrapper + input[type="submit"],
+.webform-managed-file-preview-wrapper + button,
+.webform-managed-file-preview + input[type="submit"],
+.webform-managed-file-preview + button {
+  margin: .5em 0;
+}
+
+.webform-managed-file-preview-wrapper,
+.webform-managed-file-preview-wrapper.form-item {
+  display: table;
+  border: 1px solid #ccc;
+  padding: .5em;
+}
+
+.webform-managed-file-preview,
+label.webform-managed-file-preview,
+label.option.webform-managed-file-preview {
+  display: block;
+}
+
+.webform-managed-file-placeholder,
+.webform-managed-file-preview,
+.webform-file-button {
+  margin: .5em 0;
+}
diff --git a/web/modules/webform/css/webform.element.mapping.css b/web/modules/webform/css/webform.element.mapping.css
new file mode 100644
index 0000000000000000000000000000000000000000..71f60b695863d6d95d0b3a2aae56073ccb8081a2
--- /dev/null
+++ b/web/modules/webform/css/webform.element.mapping.css
@@ -0,0 +1,13 @@
+/**
+ * @file
+ * Element mapping styles.
+ */
+
+.webform-mapping-table th {
+  width: 50%;
+}
+
+.webform-mapping-table .form-text,
+.webform-mapping-table .form-select {
+  width: 100%;
+}
diff --git a/web/modules/webform/css/webform.element.message.css b/web/modules/webform/css/webform.element.message.css
index e233c7829dfca4983a966d8f276e1396c020b0f7..2a5deb1d4820d054bdd5a5a67e37135c38239e22 100644
--- a/web/modules/webform/css/webform.element.message.css
+++ b/web/modules/webform/css/webform.element.message.css
@@ -14,7 +14,7 @@
   background-image: url(../images/icons/info.svg);
   background-repeat: no-repeat;
   background-position: 10px 17px;
-  border-color: #0074bd #0074bd #0074bd transparent;  /* LTR */
+  border-color: #0074bd #0074bd #0074bd transparent; /* LTR */
   box-shadow: -8px 0 0 #0074bd; /* LTR */
 }
 [dir="rtl"] .messages.messages--info {
diff --git a/web/modules/webform/css/webform.element.multiple.css b/web/modules/webform/css/webform.element.multiple.css
index bb3c45f3be4aa7a404f02053db6be8b6f6960173..a41ade1684b14ebaa02eea188a5dd10e2b661e89 100644
--- a/web/modules/webform/css/webform.element.multiple.css
+++ b/web/modules/webform/css/webform.element.multiple.css
@@ -20,6 +20,11 @@
   vertical-align: top;
 }
 
+.webform-multiple-table td .description,
+.webform-multiple-table td .form-item--error-message {
+  white-space: normal;
+}
+
 .webform-multiple-table td.webform-multiple-table--handle {
   padding: 0 0 0 1em;
   width: 26px;
@@ -72,6 +77,9 @@
   width: auto;
 }
 
+.webform-multiple-table td .form-type-datetime .form-type-textfield input {
+  width: 12em;
+}
 .webform-multiple-table .webform-multiple-sort-weight {
   width: 4em;
 }
@@ -86,6 +94,20 @@
   width: inherit;
 }
 
+/**
+ * Add margin between stacked form items.
+ */
+.webform-multiple-table .form-item + .form-item {
+  margin-top: .2em;
+}
+
+/**
+ * Remove margin from no messages.
+ */
+.webform-multiple-table--no-items-message .messages__wrapper {
+  padding: 0;
+}
+
 /**
  * Suppress table drag warnings.
  */
@@ -97,8 +119,16 @@
 /**
  * Tweak tabledrag toggle weight.
  */
-.webform-multiple-tabledrag-toggle-weight {
+.tabledrag-toggle-weight-wrapper.webform-multiple-tabledrag-toggle-weight {
   float: right;
+  display: inherit;
+  position: inherit;
+}
+
+.tabledrag-toggle-weight-wrapper.webform-multiple-tabledrag-toggle-weight button {
+  position: inherit;
+  top: inherit;
+  right: inherit;
 }
 
 /**
diff --git a/web/modules/webform/css/webform.element.range.css b/web/modules/webform/css/webform.element.range.css
index 9b3c73511a08377a61cead26a9a03f86609f8cbb..2062cd5294db9ce96b731accb8c90c8ab58186ef 100644
--- a/web/modules/webform/css/webform.element.range.css
+++ b/web/modules/webform/css/webform.element.range.css
@@ -6,7 +6,7 @@
  */
 
 /**
- * Verticall center range input.
+ * Vertical center range input.
  */
 .form-type-range input[type="range"] {
   display: table-cell;
diff --git a/web/modules/webform/css/webform.element.states.css b/web/modules/webform/css/webform.element.states.css
index a6c1a54bc16801f9a0c2a6ddd2638a886f9be41b..ea79b60d235a703b020b0d92e9a991c50ee73b18 100644
--- a/web/modules/webform/css/webform.element.states.css
+++ b/web/modules/webform/css/webform.element.states.css
@@ -65,3 +65,14 @@ tr.webform-states-table--state td select {
 .webform-states-table .tabledrag-changed {
   display: none !important; /* Must use !important because .tabledrag-changed 'display' is set via JavaScript */
 }
+
+/**
+ * Limit results in jQuery UI Autocomplete
+ *
+ * https://stackoverflow.com/questions/7617373/limit-results-in-jquery-ui-autocomplete
+ */
+.ui-autocomplete {
+  max-height: 200px;
+  overflow-y: auto;
+  overflow-x: hidden;
+}
diff --git a/web/modules/webform/css/webform.element.tableselect.css b/web/modules/webform/css/webform.element.tableselect.css
index aeda6cc6509fd486b335ee42d43d0099f23979d7..b93cdbd7528d04d5de2fd453eae709bfe9db89b1 100644
--- a/web/modules/webform/css/webform.element.tableselect.css
+++ b/web/modules/webform/css/webform.element.tableselect.css
@@ -8,5 +8,5 @@
 /* Make the first column containing a checkbox/radio as small as possible. */
 .tableselect th:first-child,
 .tableselect td:first-child {
-  width: 1px;
+  width: 1%;
 }
diff --git a/web/modules/webform/css/webform.element.video_file.css b/web/modules/webform/css/webform.element.video_file.css
index 3c59d6b55b4c2a6da23fe8c27b79e4983f596716..ea34d87d2eb2a1f8e66eef09a51b7775220a3433 100644
--- a/web/modules/webform/css/webform.element.video_file.css
+++ b/web/modules/webform/css/webform.element.video_file.css
@@ -5,17 +5,7 @@
  * @see /webform/test_element_media_file
  */
 
-.webform-video-file {
-  position: relative;
-  max-width: 640px;
-  height: 0;
-  padding-bottom: 56.25%;
-}
-
 .webform-video-file video {
-  position: absolute;
-  top: 0;
-  left: 0;
   width: 100%;
-  height: 100%;
+  height: auto;
 }
diff --git a/web/modules/webform/css/webform.filter.css b/web/modules/webform/css/webform.filter.css
new file mode 100644
index 0000000000000000000000000000000000000000..7453cac3e94d18dd79bf48749618a53d934faf1b
--- /dev/null
+++ b/web/modules/webform/css/webform.filter.css
@@ -0,0 +1,34 @@
+/**
+ * @file
+ * Filter styles
+ * @see /admin/structure/webform
+ * @see /admin/structure/webform/templates
+ */
+
+.webform-filter-form .form-submit {
+  margin-left: 0;
+  margin-right: 5px;
+}
+
+.webform-filter-form .form-item {
+  margin-right: 5px;
+}
+
+.webform-filter-form .form-item input {
+  max-width: 300px;
+}
+
+.webform-filter-form .form-item select {
+  max-width: 200px;
+}
+
+@media screen and (max-width: 600px) {
+  .webform-filter-form .form-item {
+    display: block;
+    margin-right: 0;
+  }
+  .webform-filter-form .form-item input,
+  .webform-filter-form .form-item select {
+    max-width: initial;
+  }
+}
diff --git a/web/modules/webform/css/webform.progress.tracker.css b/web/modules/webform/css/webform.progress.tracker.css
index b111478aa7569ed1f2c5fe04e01cc96b281a8948..4de370c6b04953829714726a20716b37a9998a3a 100644
--- a/web/modules/webform/css/webform.progress.tracker.css
+++ b/web/modules/webform/css/webform.progress.tracker.css
@@ -17,11 +17,15 @@
  * Update progress text to show completed and active.
  */
 .webform-progress-tracker .progress-step .progress-text {
-  color: #b6b6b6;
+  color: #656565;
   padding-top: 5px;
   padding-bottom: 0;
 }
 
+.webform-progress-tracker .progress-step::after {
+  background-color: #656565;
+}
+
 .webform-progress-tracker .progress-step.is-active .progress-text {
   color: #333;
 }
@@ -37,8 +41,9 @@
 /**
  * Disable hover state because webform wizard progress markers are not clickable.
  */
+.webform-progress-tracker .progress-step:not(.is-active) .progress-marker,
 .webform-progress-tracker .progress-step:hover .progress-marker {
-  background-color: #b6b6b6;
+  background-color: #656565;
 }
 
 .webform-progress-tracker .progress-step.is-complete:hover .progress-marker {
@@ -92,7 +97,7 @@
 }
 
 [dir="rtl"] .progress-tracker--center .progress-step::after {
-  right: -50%
+  right: -50%;
 }
 
 [dir="rtl"] .webform-progress-tracker .progress-step {
diff --git a/web/modules/webform/css/webform.promotions.css b/web/modules/webform/css/webform.promotions.css
index e610dbd9286f9a9235c86c31dd57e992cee6365a..8b9bff3463f717b4d04dfa8f3b4aab4ab2040614 100644
--- a/web/modules/webform/css/webform.promotions.css
+++ b/web/modules/webform/css/webform.promotions.css
@@ -6,16 +6,17 @@
 /**
  * Drupal Association.
  */
-.messages.messages--promotion_drupal_association {
+.webform-message .messages.messages--promotion_drupal_association {
   color: #333;
   background: #e0e0d8 url(../images/promotions/drupal-association-logo.png) 10px 10px no-repeat;
-  border-color: #a6a6a6 #a6a6a6 #a6a6a6 transparent;  /* LTR */
+  background-size: auto;
+  border-color: #a6a6a6 #a6a6a6 #a6a6a6 transparent; /* LTR */
   box-shadow: -8px 0 0 #a6a6a6; /* LTR */
   padding-left: 120px; /* LTR */
   min-height: 20px;
 }
 
-[dir="rtl"] .messages.messages--promotion_drupal_association {
+[dir="rtl"] .webform-message .messages.messages--promotion_drupal_association {
   border-color: #333 transparent #333 #333;
   box-shadow: 8px 0 0 #333;
   margin-left: 0;
@@ -24,7 +25,7 @@
 
 
 @media screen and (max-width: 600px) {
-  .messages.messages--promotion_drupal_association .button {
+  .webform-message .messages.messages--promotion_drupal_association .button {
     display: block;
     margin-left: 0;
     margin-right: 0;
diff --git a/web/modules/webform/css/webform.theme.bartik.css b/web/modules/webform/css/webform.theme.bartik.css
index 59c4bb898fbe10db17e5768ea711ee5b61042fd6..16002b6db4136f24a4b65840d2813f78832ce457 100644
--- a/web/modules/webform/css/webform.theme.bartik.css
+++ b/web/modules/webform/css/webform.theme.bartik.css
@@ -44,6 +44,22 @@ table {
   margin: 0;
 }
 
+/**
+ * Tweak details toggle state.
+ */
+.webform-details-toggle-state {
+  color: #0071b3;
+  text-decoration: none;
+  border-bottom: 1px dotted;
+}
+
+.webform-details-toggle-state:hover,
+.webform-details-toggle-statelink:focus {
+  color: #018fe2;
+  text-decoration: none;
+  border-bottom-style: solid;
+}
+
 /**
  * Tweak progress tracker.
  *
@@ -66,3 +82,10 @@ summary .webform-element-help {
   border-color: #999;
   background: #999;
 }
+
+/**
+ * Tweak Algolia places.
+ */
+input.ap-input {
+  font-family: "Lucida Grande", "Lucida Sans Unicode", Verdana, sans-serif;
+}
diff --git a/web/modules/webform/css/webform.theme.classy.css b/web/modules/webform/css/webform.theme.classy.css
new file mode 100644
index 0000000000000000000000000000000000000000..fabcfc830d9bbd60ae381bdf2157927bcaa85304
--- /dev/null
+++ b/web/modules/webform/css/webform.theme.classy.css
@@ -0,0 +1,14 @@
+/**
+ * @file
+ * Classy theme styles.
+ */
+
+/**
+ * Make sure the date picker is in front of the dialog.
+ *
+ * @see core/themes/classy/css/components/dialog.css
+ * @see core/themes/seven/css/components/dialog.css
+ */
+.ui-datepicker {
+  z-index: 1261 !important;
+}
diff --git a/web/modules/webform/css/webform.theme.seven.css b/web/modules/webform/css/webform.theme.seven.css
index 6ace9fb5da1dfdc1fda61d8d470b84d146b1ac22..d499c211e55467a71597626ed3c3896c0ec25f0e 100644
--- a/web/modules/webform/css/webform.theme.seven.css
+++ b/web/modules/webform/css/webform.theme.seven.css
@@ -8,34 +8,82 @@ table td {
   vertical-align: top;
 }
 
+td > .form-item:first-of-type,
+tr.odd .form-item:first-of-type,
+tr.even .form-item:first-of-type {
+  margin-top: 0;
+}
+
+td > .form-item:last-of-type,
+tr.odd .form-item:last-of-type,
+tr.even .form-item:last-of-type {
+  margin-bottom: 0;
+}
+
 /* Add margin around HR tags */
 table hr {
   margin: .5em 0;
 }
 
 /* Add background to nested details */
-details details {
+details details /* < 8.5.x  */,
+details.seven-details details.seven-details /* >= 8.6.x */ {
   background-color: #f8f8f8;
 }
 
+details details details {
+  background-color: #fff;
+}
+
 /* Reduce the width of number inputs */
 input.form-number {
   width: 6em;
 }
 
-details details details {
-  background-color: #fff;
+code {
+  font-weight: bold;
+  border: 1px solid #333;
+  background-color: #f8f8f8;
+  padding: 2px 4px;
+  color: #333;
 }
 
 /* Add yellow background to tooltips and tabs */
+/* @see core/assets/vendor/jquery.ui/themes/base/tabs.css */
 .ui-tooltip.ui-widget {
   background: #fe6;;
   border: 1px solid #ed5;
 }
 
+.webform-tabs .ui-tabs-nav .ui-tabs-anchor {
+  text-decoration: inherit;
+}
+
 .webform-tabs .ui-tabs-active.ui-state-active {
   background: #fe6;
   border: 1px solid #ed5;
+  text-decoration: none;
+}
+
+.webform-tabs .ui-state-hover {
+  text-decoration: underline !important;
+}
+
+.webform-tabs .ui-state-active.ui-state-hover {
+  text-decoration: none !important;
+}
+
+/** Compress tabs for mobile */
+@media screen and (max-width: 600px) {
+  .webform-tabs.ui-tabs .ui-tabs-nav {
+    padding: 5px 0 4px 0;
+  }
+  .webform-tabs.ui-tabs .ui-tabs-nav li {
+    padding: 0;
+  }
+  .webform-tabs.ui-tabs .ui-tabs-nav .ui-tabs-anchor {
+    padding: .5em .7em;
+  }
 }
 
 /* Accordion icon */
@@ -67,6 +115,17 @@ pre.webform-codemirror-runmode {
   }
 }
 
+/**
+ * Form action are moved to a dialogs footer and don't need to be visually
+ * hidden.
+
+ * @see Drupal.behaviors.dialog.prepareDialogButtons
+ */
+.webform-ui-dialog .form-actions input[type=submit],
+.webform-ui-dialog .form-actions a.button {
+  display: none !important;
+}
+
 /* System tray divider */
 .ui-dialog.ui-dialog-off-canvas .ui-resizable-w {
   background-color: #bfbfba;
@@ -75,6 +134,10 @@ pre.webform-codemirror-runmode {
 }
 
 /* System tray title bar */
+.ui-dialog.ui-dialog-off-canvas {
+  background: #fff;
+}
+
 .ui-dialog.ui-dialog-off-canvas .ui-dialog-titlebar {
   border-radius: 0;
 }
@@ -84,6 +147,27 @@ pre.webform-codemirror-runmode {
   margin: 1em 0;
 }
 
+/* jQuery UI autocomplete states */
+.ui-autocomplete a /* For filter autocomplete. */,
+.ui-autocomplete .ui-menu-item-wrapper /* For #states value autocomplete. */ {
+  display: block;
+  border: 1px solid transparent;
+  color: #0074bd;
+}
+
+.ui-autocomplete a.ui-state-hover,
+.ui-autocomplete a.ui-state-active,
+.ui-autocomplete a.ui-state-focus,
+.ui-autocomplete .ui-menu-item-wrapper.ui-state-hover,
+.ui-autocomplete .ui-menu-item-wrapper.ui-state-active,
+.ui-autocomplete .ui-menu-item-wrapper.ui-state-focus {
+  text-decoration: underline;
+  color: #0074bd;
+  background: #fe6;
+  border: solid 1px #ed5;
+  margin: 0;
+}
+
 /* jQuery UI button states */
 .webform-buttons .ui-button.ui-state-default {
   background: #f5f5f2;
@@ -96,7 +180,37 @@ pre.webform-codemirror-runmode {
   border: solid 1px #ed5;
 }
 
+/* jQuery UI tooltip */
+.ui-tooltip hr {
+  background: #666;
+  margin: .3em 0;
+  height: 1px;
+}
+
+.ui-tooltip code {
+  font-weight: bold;
+  white-space: nowrap;
+  background-color: #f5f5f2;
+  font-size: .9em;
+  padding: .2em;
+}
+
 /* jQuery UI tabs */
 .ui-tabs .ui-tabs-panel {
   padding: 0;
 }
+
+/* Tweak details toggle state. */
+.webform-details-toggle-state {
+  color: #0074bd;
+}
+
+.webform-details-toggle-state:hover,
+.webform-details-toggle-statelink:focus {
+  text-decoration: underline;
+}
+
+/* Token tree */
+.token-tree ul {
+  margin: 0;
+}
diff --git a/web/modules/webform/css/webform.token.css b/web/modules/webform/css/webform.token.css
new file mode 100644
index 0000000000000000000000000000000000000000..69e2f674cc32c1691996b328154083e6aee49e11
--- /dev/null
+++ b/web/modules/webform/css/webform.token.css
@@ -0,0 +1,20 @@
+/**
+ * @file
+ * Token (admin) styles
+ */
+
+tr[data-tt-id^="token-webform"] td {
+  vertical-align: top;
+}
+
+.token-tree .webform-element-more ul {
+  margin: 0;
+}
+
+tr.token-group .webform-element-more {
+  display: none;
+}
+
+tr.token-group.expanded .webform-element-more {
+  display: block;
+}
diff --git a/web/modules/webform/css/webform.wizard.pages.css b/web/modules/webform/css/webform.wizard.pages.css
new file mode 100644
index 0000000000000000000000000000000000000000..f36409e0766ec4e0371451cd06c3e3f6b81ac681
--- /dev/null
+++ b/web/modules/webform/css/webform.wizard.pages.css
@@ -0,0 +1,36 @@
+/**
+ * @file
+ * Wizard styles.
+ */
+
+.webform-wizard-pages-links {
+  display: none;
+}
+
+/**
+  * Progress link styles.
+ */
+.webform-progress [role="link"] {
+  cursor: pointer;
+}
+
+.webform-progress .progress-title[role="link"] {
+  color: #1976d2;
+}
+
+.webform-progress .progress-title[role="link"]:hover,
+.webform-progress .progress-title[role="link"]:focus {
+  text-decoration: underline;
+  color: #2196f3;
+}
+
+/**
+  * Preview link styles.
+ */
+.webform-wizard-page-edit {
+  display: none;
+}
+
+.webform-wizard-page-edit input {
+  margin: 0;
+}
diff --git a/web/modules/webform/docs/DEVELOPMENT-CHEATSHEET.md b/web/modules/webform/docs/DEVELOPMENT-CHEATSHEET.md
index 66040d5e84a378baa43cf877a30c5a6a6b4c245a..1cfecd2d81d0100a13a30fd901dfa01bf5fbde89 100644
--- a/web/modules/webform/docs/DEVELOPMENT-CHEATSHEET.md
+++ b/web/modules/webform/docs/DEVELOPMENT-CHEATSHEET.md
@@ -16,13 +16,17 @@ git diff 8.x-5.x > [project_name]-[issue-description]-[issue-number]-00.patch
 curl https://www.drupal.org/files/issues/[project_name]-[issue-description]-[issue-number]-00.patch | git apply -
 
 # Force apply patch
-patch -p1  < [project_name]-[issue-description]-[issue-number]-00.patch
+patch -p1 < 3037968-2.patch	
+
+# Remove patch and untracked files
+git reset --hard; git clean -f -d
 
 # Create interdiff
 interdiff \
   [issue-number]-[old-comment-number].patch \
   [issue-number]-[new-comment-number].patch \
   > interdiff-[issue-number]-[old-comment-number]-[new-comment-number].txt
+cat interdiff-[issue-number]-[old-comment-number]-[new-comment-number].txt
 
 # Merge branch with all commits
 git checkout 8.x-5.x
@@ -35,9 +39,12 @@ git merge --squash [issue-number]-[issue-description]
 git commit -m 'Issue #[issue-number]: [issue-description]'
 git push
 
-# Delete branch
+# Delete local and remote branch
 git branch -D [issue-number]-[issue-description]
 git push origin :[issue-number]-[issue-description]
+
+# Delete remote branch
+git push origin --delete [issue-number]-[issue-description]
 ```
 
 **Generate Drush Make and Composer Files**
@@ -50,7 +57,7 @@ drush webform-libraries-composer > composer.json
 **Manually Execute an Update Hook**
 
 ```bash
-drush php-eval 'module_load_include('install', 'webform'); webform_update_8032()';
+drush php-eval 'module_load_include('install', 'webform'); webform_update_8144()';
 ```
 
 **Import and Export Configuration**
@@ -60,24 +67,33 @@ drush php-eval 'module_load_include('install', 'webform'); webform_update_8032()
 # These files will be ignored. @see .gitignore.
 echo 'true' > webform.features.yml
 
+echo 'true' > modules/webform_attachment/webform_attachment.features.yml
+echo 'true' > modules/webform_attachment/tests/modules/webform_attachment_test/webform_attachment_test.features.yml
+
 echo 'true' > modules/webform_examples/webform_examples.features.yml
+echo 'true' > modules/webform_examples_accessibility/webform_examples_accessibility.features.yml
 echo 'true' > modules/webform_example_element/webform_example_element.features.yml
 echo 'true' > modules/webform_example_composite/webform_example_composite.features.yml
+echo 'true' > modules/webform_example_handler/webform_example_handler.features.yml
 echo 'true' > modules/webform_example_element/webform_example_remote_post.features.yml
 
 echo 'true' > modules/webform_templates/webform_templates.features.yml
 echo 'true' > modules/webform_templates/webform_templates.features.yml
 
 echo 'true' > modules/webform_image_select/webform_image_select.features.yml
-echo 'true' > modules/webform_image_select/tests/modules/webform_image_select_test.features.yml
+echo 'true' > modules/webform_image_select/tests/modules/webform_image_select_test/webform_image_select_test.features.yml
 
 echo 'true' > modules/webform_node/webform_node.features.yml
 echo 'true' > modules/webform_node/tests/modules/webform_node_test_multiple/webform_node_test_multiple.features.yml
+echo 'true' > modules/webform_node/tests/modules/webform_node_test_translation/webform_node_test_translation.features.yml
 
 echo 'true' > modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test/webform_scheduled_email_test.features.yml
 
+echo 'true' > modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/webform_submission_export_import_test.features.yml
+
 echo 'true' > modules/webform_demo/webform_demo_application_evaluation/webform_demo_application_evaluation.features.yml
 echo 'true' > modules/webform_demo/webform_demo_event_registration/webform_demo_event_registration.features.yml
+echo 'true' > modules/webform_demo/webform_demo_region_contact/webform_demo_region_contact.features.yml
 
 echo 'true' > tests/modules/webform_test/webform_test.features.yml
 echo 'true' > tests/modules/webform_test_ajax/webform_test_ajax.features.yml
@@ -88,10 +104,12 @@ echo 'true' > tests/modules/webform_test_block_submission_limit/webform_test_blo
 echo 'true' > tests/modules/webform_test_config_performance/webform_test_config_performance.features.yml
 echo 'true' > tests/modules/webform_test_custom_properties/webform_test_custom_properties.features.yml
 echo 'true' > tests/modules/webform_test_element/webform_test_element.features.yml
+echo 'true' > tests/modules/webform_test_entity_reference_views/webform_test_entity_reference_views.features.yml
 echo 'true' > tests/modules/webform_test_handler/webform_test_handler.features.yml
 echo 'true' > tests/modules/webform_test_handler_remote_post/webform_test_handler_remote_post.features.yml
 echo 'true' > tests/modules/webform_test_options/webform_test_options.features.yml
 echo 'true' > tests/modules/webform_test_paragraphs/webform_test_paragraphs.features.yml
+echo 'true' > tests/modules/webform_test_rest/webform_test_rest.features.yml
 echo 'true' > tests/modules/webform_test_submissions/webform_test_submissions.features.yml
 echo 'true' > tests/modules/webform_test_third_party_settings/webform_test_third_party_settings.features.yml
 echo 'true' > tests/modules/webform_test_translation/webform_test_translation.features.yml
@@ -102,106 +120,147 @@ echo 'true' > tests/modules/webform_test_wizard_custom/webform_test_wizard_custo
 
 # Make sure all modules that are going to be exported are enabled
 drush en -y webform\
+  webform_attachment\
   webform_demo_application_evaluation\
   webform_demo_event_registration\
+  webform_demo_region_contact\
   webform_examples\
-  webform_examples\
+  webform_examples_accessibility\
   webform_example_element\
+  webform_example_handler\
   webform_example_remote_post\
   webform_image_select\
   webform_node\
+  webform_submission_export_import\
   webform_templates\
   webform_test\
   webform_test_element\
+  webform_test_entity_reference_views\  
   webform_test_handler\
   webform_test_handler_remote_post\
   webform_test_options\
-  webform_test_views\
+  webform_test_paragraphs\
+  webform_test_rest\
   webform_test_submissions\
   webform_test_translation\
+  webform_test_views\
+  webform_attachment_test\
   webform_image_select_test\
   webform_node_test_multiple\
-  webform_scheduled_email_test;
+  webform_node_test_translation\
+  webform_scheduled_email_test\
+  webform_submission_export_import_test;
 
 # Show the difference between the active config and the default config.
 drush features-diff webform
 drush features-diff webform_test
 
-# Export webform configuration from your site.          
+# Export webform configuration from your site.
 drush features-export -y webform
+drush features-export -y webform_attachment
 drush features-export -y webform_demo_application_evaluation
 drush features-export -y webform_demo_event_registration
+drush features-export -y webform_demo_region_contact
 drush features-export -y webform_examples
+drush features-export -y webform_examples_accessibility
 drush features-export -y webform_example_element
 drush features-export -y webform_example_composite
+drush features-export -y webform_example_handler
 drush features-export -y webform_example_remote_post
 drush features-export -y webform_node
 drush features-export -y webform_image_select
+drush features-export -y webform_submission_export_import
 drush features-export -y webform_templates
 drush features-export -y webform_test
 drush features-export -y webform_test_block_submission_limit
 drush features-export -y webform_test_element
+drush features-export -y webform_test_entity_reference_views
 drush features-export -y webform_test_handler
 drush features-export -y webform_test_handler_remote_post
 drush features-export -y webform_test_options
-drush features-export -y webform_test_views
+drush features-export -y webform_test_rest
 drush features-export -y webform_test_submissions
 drush features-export -y webform_test_translation
+drush features-export -y webform_test_views
 drush features-export -y webform_test_paragraphs
+drush features-export -y webform_attachment_test
 drush features-export -y webform_image_select_test
 drush features-export -y webform_node_test_multiple
+drush features-export -y webform_node_test_translation
 drush features-export -y webform_scheduled_email_test
+drush features-export -y webform_submission_export_import_test
 
 # Revert all feature update to *.info.yml files.
 git checkout -- *.info.yml
 
-# Tidy webform configuration from your site.          
+# Tidy webform configuration from your site.
 drush webform:tidy -y --dependencies webform
+drush webform:tidy -y --dependencies webform_attachment
 drush webform:tidy -y --dependencies webform_demo_application_evaluation
 drush webform:tidy -y --dependencies webform_demo_event_registration
+drush webform:tidy -y --dependencies webform_demo_region_contact
 drush webform:tidy -y --dependencies webform_examples
+drush webform:tidy -y --dependencies webform_examples_accessibility
 drush webform:tidy -y --dependencies webform_example_element
 drush webform:tidy -y --dependencies webform_example_composite
+drush webform:tidy -y --dependencies webform_example_handler
 drush webform:tidy -y --dependencies webform_example_remote_post
 drush webform:tidy -y --dependencies webform_image_select
 drush webform:tidy -y --dependencies webform_node
+drush webform:tidy -y --dependencies webform_submission_export_import
 drush webform:tidy -y --dependencies webform_templates
 drush webform:tidy -y --dependencies webform_test
 drush webform:tidy -y --dependencies webform_test_block_submission_limit
 drush webform:tidy -y --dependencies webform_test_element
+drush webform:tidy -y --dependencies webform_test_entity_reference_views
 drush webform:tidy -y --dependencies webform_test_handler
 drush webform:tidy -y --dependencies webform_test_handler_remote_post
 drush webform:tidy -y --dependencies webform_test_options
-drush webform:tidy -y --dependencies webform_test_views
+drush webform:tidy -y --dependencies webform_test_paragraphs
+drush webform:tidy -y --dependencies webform_test_rest
 drush webform:tidy -y --dependencies webform_test_submissions
 drush webform:tidy -y --dependencies webform_test_translation
-drush webform:tidy -y --dependencies webform_test_paragraphs
+drush webform:tidy -y --dependencies webform_test_views
+drush webform:tidy -y --dependencies webform_attachment_test
 drush webform:tidy -y --dependencies webform_image_select_test
 drush webform:tidy -y --dependencies webform_node_test_multiple
+drush webform:tidy -y --dependencies webform_node_test_translation
 drush webform:tidy -y --dependencies webform_scheduled_email_test
+drush webform:tidy -y --dependencies webform_submission_export_import_test
 
-# Re-import all webform configuration into your site.      
+# Re-import all webform configuration into your site.
 drush features-import -y webform
+drush features-import -y webform_attachment
 drush features-import -y webform_demo_application_evaluation
 drush features-import -y webform_demo_event_registration
+drush features-import -y webform_demo_region_contact
 drush features-import -y webform_examples
+drush features-import -y webform_examples_accessibility
 drush features-import -y webform_example_element
 drush features-import -y webform_example_composite
+drush features-import -y webform_example_handler
 drush features-import -y webform_example_remote_post
 drush features-import -y webform_node
 drush features-import -y webform_image_select
+drush features-import -y webform_submission_export_import
 drush features-import -y webform_templates
 drush features-import -y webform_test
 drush features-import -y webform_test_element
+drush features-import -y webform_test_entity_reference_views
 drush features-import -y webform_test_block_submission_limit
 drush features-import -y webform_test_handler
 drush features-import -y webform_test_handler_remote_post
 drush features-import -y webform_test_options
-drush features-import -y webform_test_views
+drush features-import -y webform_test_paragraphs
+drush features-import -y webform_test_rest
 drush features-import -y webform_test_submissions
 drush features-import -y webform_test_translation
-drush features-import -y webform_test_paragraphs
+drush features-import -y webform_test_views
+drush features-import -y webform_attachment_test
 drush features-import -y webform_image_select_test
 drush features-import -y webform_node_test_multiple
+drush features-import -y webform_node_test_translation
 drush features-import -y webform_scheduled_email_test
+drush features-import -y webform_submission_export_import_test
+
 ```
diff --git a/web/modules/webform/docs/DEVELOPMENT-NOTES.md b/web/modules/webform/docs/DEVELOPMENT-NOTES.md
index 91a0b43cd384ab03517429332c959ec5929b459c..8c2bdeae2a8c8decfb07bd6838b8976ac61a9bc6 100644
--- a/web/modules/webform/docs/DEVELOPMENT-NOTES.md
+++ b/web/modules/webform/docs/DEVELOPMENT-NOTES.md
@@ -73,7 +73,7 @@ git reset --hard
 git commit --amendd ../
 
 # Unstage a file about to be committed
-git reset HEAD <file>...
+git reset HEAD <file>…
 
 # Revert (in SVN terms) an uncommitted file to the copy in your latest commit
 git checkout -- filename
diff --git a/web/modules/webform/docs/FEATURES.md b/web/modules/webform/docs/FEATURES.md
index ab2bc32d8f03dafe3388f78488519ff3777ca78e..9a4aec0bd9ab3f86f488e3be00883d2ea86956aa 100644
--- a/web/modules/webform/docs/FEATURES.md
+++ b/web/modules/webform/docs/FEATURES.md
@@ -1,219 +1,460 @@
 Features
 --------
 
-## Form Builder
 
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-builder.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-builder.png" alt="Form Builder" />
-</a>
+<blockquote>
+The Webform module provides all the features expected from an enterprise 
+proprietary form builder combined with the flexibility and openness of Drupal
+</blockquote>
+
+The Webform module allows you to build any type of form that can collect any
+type of data, which can be submitted to any application or system.
+Every single behavior and aspect of your forms and its inputs are customizable.
+Whether you need a multi-page form containing a multi-column input layout with
+conditional logic or a simple contact form that pushes data to a SalesForce/CRM,
+ it is all possible using the Webform module for Drupal 8.
+
+Drupal and the Webform module strives to be fully accessible to all users and 
+site builders. Assistive technologies, including screen readers and
+keyboard access, are fully supported.
+
+Besides being a feature rich form builder, the Webform module is part of the 
+Drupal project's ecosystem and community.
+
+<blockquote>
+The <a href="https://www.drupal.org/about">Drupal project</a> is open source software. 
+Anyone can download, use, work on, and share it with others. 
+It's built on principles like collaboration, globalism, and innovation. 
+It's distributed under the terms of the <a href="https://www.gnu.org/copyleft/gpl.html">GNU General Public License</a> (GPL). 
+There are <a href="https://www.drupal.org/about/licensing">no licensing fees</a>, ever. Drupal (and Webform) will always be free.
+</blockquote>
+
+<div align="center">
+<table class="views-view-grid">
+  <tr>
+<td><a class="action-button" href="https://youtu.be/VncMRSwjVto">▶ Watch video</a></td>
+<td><a class="action-button" href="https://simplytest.me/project/webform/8.x-5.x">Try Webform</a></td>
+  </tr>
+</table>
 </div>
 
-The Webform module provides an intuitive webform builder based upon Drupal 8's 
-best practices for user interface and user experience. The webform builder allows non-technical users to easily build and maintain webforms.
+<hr/>
+
+## Form manager
+
+<table class="views-view-grid" width="100%">
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--form-manager.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--form-manager.png" alt="Form manager" /><br/>
+    <strong>Form manager</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--form-manager-add.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--form-manager-add.png" alt="Form manager: Add webform" /><br/>
+    <strong>Add webform</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+The form manager provides a list of all available webforms.
+
+Form manager features include:
+
+- Filtering by keyword, category, and status
+- Sorting by total number of submissions
+- Archiving of old forms
+
+## Form builder
+
+<table class="views-view-grid" width="100%">
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--form-builder.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--form-builder.png" alt="Form builder" /><br/>
+    <strong>Form builder</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--form-builder.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--form-builder.png" alt="Edit element" /><br/>
+    <strong>Edit element</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+The Webform module provides an intuitive form builder based upon Drupal 8's 
+best practices for user interface design and user experience. 
+The form builder allows non-technical users to easily build 
+and maintain webforms.
 
 Form builder features include:
 
-- Drag-n-drop webform element management
-- Generation of test submissions
-- Duplication of existing webforms, templates, and elements
+- Drag-n-drop form element management
+- Multi-column layout management
+- Conditional logic overview
+- Element duplication
+
+## Configuration settings
+
+Form behaviors, features, submission handling, messaging, and confirmations are completely customizable using global settings and/or form-specific settings.
+
+<table class="views-view-grid" width="100%">
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--settings-general.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--settings-general.png" alt="Configuration settings: General" /><br/>
+    <strong>General</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--settings-form.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--settings-form.png" alt="Configuration settings: Form" /><br/>
+    <strong>Form</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+### General settings
+
+Allow a webform's administrative information, paths, behaviors, and third-party settings to be customized. 
+
+General settings include:
+
+- Categorization
+- Customizable paths
+- Disable saving of results
+- Ajax support
+
+### Form settings
+
+Allow a form's status, attributes, behaviors, labels, messages, wizard settings,
+and preview to be customized. 
+
+Form settings include:
+
+- Open and close date/time scheduling
+- Login redirection with custom messaging.
+- Multiple step wizard forms
+- Submission preview
+- Input prepopulation using query string parameters.
+
+<table class="views-view-grid" width="100%">  
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--settings-submissions.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--settings-submissions.png" alt="Configuration settings: Submisssions" /><br/>
+    <strong>Submisssions</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--settings-confirmation.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--settings-confirmation.png" alt="Configuration settings: Confirmation" /><br/>
+    <strong>Confirmation</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+### Submissions settings
+
+Allows a submission's labels, behaviors, limits, and draft settings to be 
+customized. 
+
+Submission settings include:
+
+- Saving of drafts
+- Automatic purging of submissions
+- Submission limits per user and/or per form
+- Autofilling form using previously submitted values
+
+### Confirmation settings
+
+Allows the form's confirmation type, message, and URL to be customized. 
+
+Confirmation types include:
+
+- Dedicated page
+- Redirect to internal or external URL
+- Displaying of a custom status message
+- Opening a modal dialog
+
+<table class="views-view-grid" width="100%">  
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--settings-handlers.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--settings-handlers.png" alt="Configuration settings: Handlers" /><br/>
+    <strong>Handlers</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--settings-handlers-email.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--settings-handlers-email.png" alt="Configuration settings: Email handler" /><br/>
+    <strong>Email handler</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+
+### Emails / Handlers
+
+Allows additional actions and behaviors to be processed when a webform or 
+submission is created, updated, or deleted. Handlers are used to route 
+submitted data to external applications and send notifications & confirmations. 
 
+Email support features include:
 
-## Form Settings
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-settings.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-settings-thumbnail.png" alt="Form Settings" />
-</a>
-</div>
+- Previewing and resending emails
+- Sending HTML emails
+- File attachments (requires the Mail System and Swift Mailer module.)
+- HTML and plain-text email-friendly Twig templates
+- Customizable display formats for individual form elements
 
-Form submission handling, messaging, and confirmations are completely 
-customizable using global settings and/or form-specific settings.
- 
-Form settings that can be customized include:
-
-- Messages and button labels
-- Confirmation page, messages, and redirects
-- Saving drafts
-- Previewing submissions
-- Confidential submissions
-- Prepopulating a webform's elements using query string parameters
-- Preventing duplicate submissions 
-- Disabling back button
-- Warning users about unsaved changes
-- Disabling client-side validation
-- Limiting number of submission per user, per webform, and/or per node
-- Look-n-feel of webform, confirmation page, and buttons
-- Injection webform specific CSS and JavaScript
+Remote post features include:
+- Posting selected elements to a remote server
+- Adding custom parameters to remote post requests
 
+<table class="views-view-grid" width="100%">  
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--settings-assets.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--settings-assets.png" alt="Configuration settings: CSS/JS assets" /><br/>
+    <strong>CSS/JS assets</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--settings-access.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--settings-access.png" alt="Configuration settings: Access" /><br/>
+    <strong>Access</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+### CSS/JS assets
+
+The CSS/JS assets page allows site builders to attach custom CSS and JavaScript
+to a webform.  Custom CSS can be used to make simple layout or design tweaks 
+to a form. Custom JavaScript allows additional conditional logic and 
+behaviors to be added to a form.
+
+### Access settings
+
+Allows an administrator to determine who can administer a webform and/or create, 
+update, delete, and purge webform submissions. 
 
 ## Elements
 
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-elements.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-elements-thumbnail.png" alt="Elements" />
-</a>
-</div>
-
-The Webform module is built directly on top of Drupal 8's Form API. Every
-[form element](https://api.drupal.org/api/drupal/developer!topics!forms_api_reference.html/8) 
-available in Drupal 8 is supported by the Webform module.
+<table class="views-view-grid" width="100%">
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--elements.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--elements.png" alt="Elements" /><br/>
+    <strong>Elements</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--elements-settings.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--elements-settings.png" alt="Element settings" /><br/>
+    <strong>Element settings</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>  
+
+The Webform module is built directly on top of Drupal 8's Form API. 
+Every form element available in Drupal 8 is supported by the Webform module.
 
 Form elements include:
 
-- **HTML:** Textfield, Textareas, Checkboxes, Radios, Select menu, 
-  Password, and more...
-- **HTML5:** Email, Url, Number, Telephone, Date, Number, Range, 
-  and more...
-- **Drupal specific** File uploads, Entity References, Table select, Date list, 
-  and more...
-- **Custom:** [Likert scale](https://en.wikipedia.org/wiki/Likert_scale), 
-  Star rating, Toggle, Buttons, Geolocation, 
-  Select/Checkboxes/Radios with other, and more...
-- **Markups** Inline dismissable messages, HTML Markup, Details, and Fieldsets.   
-- **Composite elements:** Name, Address, and Contact 
-
-
-## Element Settings
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-element-settings.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-element-settings-thumbnail.png" alt="Element Settings" />
-</a>
-</div>
-
-All of Drupal 8's default webform element properties and behaviors are supported. 
-There are also several custom webform element properties and settings
-available to enhance a webform element's behavior.
- 
-Standard and custom properties allow for:
-
-- **Customizable required error messages**
-- **Conditional logic** using [FAPI States API](https://api.drupal.org/api/examples/form_example%21form_example_states.inc/function/form_example_states_form/7)
-- **Input masks** (using [jquery.inputmask](https://github.com/RobinHerbots/jquery.inputmask))
-- **[Select2](https://select2.github.io/)** replacement of select boxes 
-- **Word and character counting** for text elements
-- **Help popup** (using [jQuery UI Tooltip](https://jqueryui.com/tooltip/))
-- **Regular expression pattern validation**
-- **Private** elements, visible only to administrators
-- **Unique** values per element
+- **Basic HTML**: Textfield, Textareas, Checkboxes, Radios, Select menu, Password, and more...
+- **Advanced HTML5**: Email, Url, Number, Telephone, Date, Number, Range, and more...
+- **Advanced Drupal**: File uploads, Entity References, Table select, Date list, and more...
+- **Widgets**: Likert scale, Star rating, Buttons, Geolocation, Terms of service, Select/Checkboxes/Radios with other, and more...
+- **Markup**: Dismissible messages, Basic HTML, Advanced HTML, Details, and Fieldsets.
+- **Composites**: Name, Address, Contact, Credit Card, and event custom composites
+- **Computed**: Calculated values using Tokens and Twig with Ajax support.
 
+### Element settings
 
-## Viewing Source
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-source.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-source-thumbnail.png" alt="Viewing Source" />
-</a>
-</div>
-
-At the heart of a Webform module's webform elements is a Drupal render array,
-which can be edited and managed by developers. The Drupal render array gives developers
-complete control over a webform's elements, layout, and look-and-feel by
-allowing developers to make bulk updates to a webform's label, descriptions, and 
-behaviors.
+All of Drupal 8's default form element properties and behaviors are supported. 
+There are also several custom webform element properties and settings available
+to enhance a form element's behavior.
 
+Standard and custom properties allow for:
 
-## States/Conditional Logic
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-states.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-states.png" alt="States/Conditional Logic" />
-</a>
-</div>
-
-Drupal's State API can be used by developers to provide conditional logic to 
-hide and show webform elements.
+- Customizable error validation messages
+- Conditional logic using [FAPI States API](https://api.drupal.org/api/examples/form_example%21form_example_states.inc/function/form_example_states_form/7)
+- Input masks (using [jquery.inputmask](https://github.com/RobinHerbots/jquery.inputmask))
+- [Select2](https://select2.github.io/) or [Chosen](https://harvesthq.github.io/chosen/) replacement of select boxes
+- Word and character counting for text elements
+- Help tooltips (using [jQuery UI Tooltip](https://jqueryui.com/tooltip/))
+- More information slideouts
+- Regular expression pattern validation
+- Private elements, visible only to administrators
+- Unique values per element
+
+<table class="views-view-grid" width="100%">  
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--elements-conditional.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--elements-conditional.png" alt="Conditional logic" /><br/>
+    <strong>Conditional logic</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--elements-source.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--elements-source.png" alt="View source" /><br/>
+    <strong>View source</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+### States/Conditional logic
+
+Drupal's State API can be used by developers to provide conditional logic 
+to hide and show form elements.
 
 Drupal's State API supports:
 
 - Show/Hide
+- Required/Optional
 - Open/Close
 - Enable/Disable
 
-
-## Multistep Forms
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-wizard.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-wizard.png" alt="Multistep Forms" />
-</a>
-</div>
-
-Forms can be broken up into multiple pages using a progress bar. Authenticated
-users can save drafts and/or have their changes automatically saved as they 
-progress through a long webform.
-
-Multistep webform features include:
+### Viewing source
+
+At the heart of a Webform module's form elements is a Drupal 
+[render array](https://www.drupal.org/docs/8/api/render-api/render-arrays), 
+which can be edited and managed by developers. The Drupal render array 
+gives developers complete control over a webform's elements, layout,
+and look-and-feel by allowing developers to make bulk updates 
+to a webform's label, descriptions, and behaviors.
+
+## Forms
+
+<table class="views-view-grid" width="100%">
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--forms-accessiblity.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--forms-accessiblity.png" alt="Accessibility" /><br/>
+    <strong>Accessibility</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--forms-wizard.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--forms-wizard.png" alt="Multistep form" /><br/>
+    <strong>Multistep form</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+### Accessibility
+
+The outputted forms and even the Webform module's administrative interface 
+(i.e. form builder) are accessible using keyboard navigation and screen readers.
+The Webform module complies with [WCAG 2.0](http://www.w3.org/TR/WCAG20/#contents)
+and [ATAG 2.0](http://www.w3.org/TR/ATAG20/#contents) guidelines.
+
+### Multistep form
+
+Forms can be broken up into multiple pages using a progress bar. 
+Authenticated users can save drafts and/or have their changes automatically 
+saved as they progress through a long form.
+
+Multistep form features include:
 
 - Customizable progress bar
 - Customizable previous and next button labels and styles
 - Saving drafts between steps
 
-
-## Email & Remote Post Handlers
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-handlers.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-handlers-thumbnail.png" alt="Email/Handlers" />
-</a>
-</div>
-
-Upon webform submission, customizable email notifications and confirmations can
-be sent to users and administrators. 
-
-An extendable plugin that allows developers to push submitted data 
-to external or internal systems and/or applications is provided. 
-
-Email support features include:
-
-- Previewing and resending emails
-- Sending HTML emails
-- File attachments (requires the [Mail System](https://www.drupal.org/project/mailsystem) and [Swift Mailer](https://www.drupal.org/project/swiftmailer) module.) 
-- HTML and plain-text email-friendly Twig templates
-- Customizable display formats for individual webform elements
-
-Remote post features include:
-
-- Posting selected elements to remote server
-- Adding custom parameters to remote post requests
-
-
-## Results Management
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-results.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-results-thumbnail.png" alt="Results Management" />
-</a>
-</div>
-
-Form submissions can optionally be stored in the database, reviewed, and
-downloaded.  
-
-Submissions can also be flagged with administrative notes.
+### Drupal integration​
+
+<table class="views-view-grid" width="100%">
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--forms-block.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--forms-block.png" alt="Block" /><br/>
+    <strong>Block</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--forms-node.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--forms-node.png" alt="Node" /><br/>
+    <strong>Node</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+Webforms can be attached to nodes or displayed as blocks. 
+Webforms can also have dedicated SEO-friendly URLs.
+Form elements are render arrays that can easily be altered using 
+custom hooks and/or plugins.
+
+## Results management
+
+<table class="views-view-grid" width="100%">
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--results.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--results.png" alt="Results" /><br/>
+    <strong>Results</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--results-customize.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--results-customize.png" alt="Customize results" /><br/>
+    <strong>Customize results </strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+Form submissions can optionally be stored in the database, reviewed, 
+and downloaded. Submissions can also be flagged with administrative notes.
 
 Results management features include:
 
-- Flagging
-- Administrative notes 
+- Submission flagging, locking, and notes
 - Viewing submissions as HTML, plain text, and YAML
 - Customizable reports
 - Downloading results as a CSV to Google Sheets or MS Excel
 - Saving of download preferences per form
-- Automatically purging old submissions based on certain criteria
-
-
-## Access Controls
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-access.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-access-thumbnail.png" alt="Access Controls" />
-</a>
-</div>
-
-The Webform module provides full access controls for managing who can create
-forms, post submissions, and access a webform's results.  
-Access controls can be applied to roles and/or specific users.
+- [Drupal Views](https://www.drupal.org/docs/8/core/modules/views) integration 
+for advanced reporting.
+
+## Access controls
+
+<table class="views-view-grid" width="100%">
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--access-permissions.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--access-permissions.png" alt="Permissions" /><br/>
+    <strong>Permissions</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--access-element.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--access-element.png" alt="Element access" /><br/>
+    <strong>Element access</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+The Webform module provides full access controls and permissions for managing 
+who can create forms, post submissions, and access a webform's results. 
+Access controls can be applied to roles and/or specific users. 
+The Webform access submodule allows you to even setup reusable permission 
+groups which can be applied to multiple instances of the same webform.
 
 Access controls allow users to:
 
@@ -226,162 +467,168 @@ Access controls allow users to:
 - View selected elements
 - Update selected elements
 
-
-## Reusable Templates
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-templates.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-templates-thumbnail.png" alt="Reusable Templates" />
-</a>
-</div>
-
-The Webform module provides a few starter templates and multiple example forms
-which webform administrators can update or use to create new reusable templates
-for their organization.
+## Reusable templates
+
+<table class="views-view-grid" width="100%">
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--templates.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--templates.png" alt="Templates" /><br/>
+    <strong>Templates</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--templates-preview.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--templates-preview.png" alt="Templates preview" /><br/>
+    <strong>Templates preview</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+The Webform module provides a few starter templates and multiple example 
+forms which webform administrators can update or use to create new 
+reusable templates for their organization.
 
 Starter templates include:
 
-- Contact Us	
+- Contact Us
 - Donation
 - Employee Evaluation
 - Issue
-- Job Application	
+- Job Application
 - Job Seeker Profile
 - Registration
 - Session Evaluation
 - Subscribe
 - User Profile
 
-Example webforms include:
-
-- Elements
-- Basic layout
-- Flexbox layout
-- Input masks
-- Options
-- Wizard
-
-
-## Reusable Options
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-options.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-options-thumbnail.png" alt="Reusable Options" />
-</a>
-</div>
-
-Administrators can define reusable global options for select menus, checkboxes, 
-and radio buttons. The Webform module includes default options for states,
-countries, [likert](https://en.wikipedia.org/wiki/Likert_scale) answers, 
-and more.   
+## Reusable options
+
+<table class="views-view-grid" width="100%">
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--options.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--options.png" alt="Options" /><br/>
+    <strong>Options</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--options-likert.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--options-likert.png" alt="Likert" /><br/>
+    <strong>Likert</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+
+Administrators can define reusable global options for select menus, checkboxes,
+and radio buttons. The Webform module includes default options for states, 
+countries, demographics, likert answers, and more.
 
 Reusable options include:
 
-- Country codes & names	
-- Days, Months, Time zones
-- Education, Employment status, Ethnicity, Industry, Languages, Marital status, Relationship, Size, and Titles
-- Likert agreement, comparison, importance, quality, satisfaction, ten scale, and
-  would you
-- State/province codes & names	
-- State codes	& names		
+- **Geographic**: Languages, country, and states
+- **Date and time**: Days, months, and time zones
+- **Demographic**: Education, employment status, ethnicity, Industry, 
+languages, marital status, relationship, size, and job titles
+- **Likert**: Agreement, comparison, importance, quality, satisfaction, 
+ten scale, and would you
 
 
 ## Internationalization
-    
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-internalization.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-internalization-thumbnail.png" alt="Internationalization" />
-</a>
-</div>
-
-Forms and configuration can be translated into multiple languages using Drupal's
-configuration translation system.    
-
-
-## Drupal Integration
 
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-integration.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-integration-thumbnail.png" alt="Drupal Integration" />
-</a>
-</div>
-
-Forms can be attached to nodes or displayed as blocks. Webforms can also have
-dedicated SEO-friendly URLs. Webform elements are simply render arrays that can
-easily be altered using custom hooks and/or plugins.
-
-
-## Add-ons & Third Party Settings
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-add-ons.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-add-ons-thumbnail.png" alt="Add-ons & Third Party Settings" />
-</a>
-</div>
-
-Includes a list of modules and projects that extend and/or provide additional 
-functionality to the Webform module and Drupal's Form API.
-
-
-## Extendable Plugins
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-plugin.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-plugin.png" alt="Extendable Plugins" />
-</a>
-</div>
-
-The Webform module provides [plugins](https://www.drupal.org/developing/api/8/plugins)
-and hooks that allow contrib and custom modules to extend and enhance webform 
-elements and submission handling.
-
-**WebformElement plugin** is used to integrate and enhance webform elements so 
-that they can be properly integrated into the Webform module.
-
-**WebformHandler plugin** allows developers to extend a webform's submission 
-handling. 
-
-**WebformExporter plugin** allows developers to export results using custom
-formats and file types. 
-
-
-## Help & Video Tutorials
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-help.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-help-thumbnail.png" alt="Help & Video Tutorials" />
-</a>
-</div>
-
-The Webform module provides examples, inline help, and screencast walk throughs.
-
-Screencasts include:
- 
-- [Welcome to the Webform module](https://youtu.be/sQGsfQ_LZJ4)
-- [Installing the Webform module and third party libraries](https://youtu.be/IMfFTrsjg5k)
-- [Managing Webforms, Templates, and Examples](https://youtu.be/T5MVGa_3jOQ)
-- [Adding Elements, Composites, and Containers](https://youtu.be/LspF9mAvRcY)
-- [Configuring Webform Settings and Behaviors](https://youtu.be/UJ0y09ZS9Uc)
-- [Controlling Access to Webforms and Elements](https://youtu.be/SFm76DAVjbE)
-- [Collecting Submissions, Sending Emails, and Posting Results](https://youtu.be/OdfVm5LMH9A)
-- [Placing Webforms in Blocks and Creating Webform Nodes](https://youtu.be/xYBW2g0osd4)
-- [Administering and Extending the Webform module](https://youtu.be/bkScAX_Qbt4)
-- [Using the Source](https://youtu.be/2pWkJiYeR6E)
-- [Getting Help](https://youtu.be/sRXUR2c2brA) 
-
-
-## Drush Integration
-
-<div class="thumbnail">
-<a href="https://www.drupal.org/files/webform-8.x-5.x-drush.png">
-<img src="https://www.drupal.org/files/webform-8.x-5.x-drush.png" alt="Drush Integration" />
-</a>
-</div>
+Forms and configuration can be translated into multiple languages using 
+Drupal's [configuration translation system](https://www.drupal.org/docs/8/core/modules/config-translation.
+
+## Add-ons
+
+<table class="views-view-grid" width="100%">
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--addons.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--addons.png" alt="Add-ons" /><br/>
+    <strong>Add-ons</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--addons-webform-analysis.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--addons-webform-analysis.png" alt="Analysis" /><br/>
+    <strong>Analysis</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+There are [dozens of add-ons available](https://www.drupal.org/node/2837065)
+that extend and/or provide additional functionality to the Webform module 
+and Drupal's Form API. 
+
+Add-ons include:
+
+- Analysis for creating graphs and charts
+- CRM integration including SalesForce, HubSpot, MyEmma, SugarCRM, more…
+- SPAM protection 
+- Advanced workflows
+- Data encryption
+- GDPR compliance
+
+## Development tools
+
+<table class="views-view-grid" width="100%">
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--devel-test.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--devel-test.png" alt="Test" /><br/>
+    <strong>Test</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--devel-api.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--devel-api.png" alt="API" /><br/>
+    <strong>API</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
+
+Examples and tools are provided to help developers get started customizing 
+existing features and adding new features to the Webform module.
+
+Development tools include:
+
+- Test forms using customizable default values
+- Easy to export configuration files
+- Debugging tools for all handlers
+- Examples for external API integration using remotes posts or custom code
+- Example modules for creating custom elements and handlers
+- Demos for building event registration and application evaluation system
+
+## Drush & Composer integration
+
+<table class="views-view-grid" width="100%">
+  <tr>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--drush.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--drush.png" alt="Drush" /><br/>
+    <strong>Drush</strong>
+    </a>
+    </div></td>
+    <td width="50%"><div class="note">
+    <a href="https://www.drupal.org/files/webform-8.x.5.x-features--drush-composer.png">
+    <img src="https://www.drupal.org/files/webform-8.x.5.x-features--drush-composer.png" alt="Composer" /><br/>
+    <strong>Composer</strong>
+    </a>
+    </div></td>
+  </tr>
+</table>
 
 Drush commands are provided to:
 
-- Generate multiple webform submissions
-- Export webform submissions
-- Purge webform submissions
-- Download and manage third party libraries
+- Generate multiple submissions
+- Export submissions
+- Purge submissions
+- Download and manage third-party libraries
+- Generate a composer.js file for third-party libraries
 - Tidy YAML configuration files
diff --git a/web/modules/webform/docs/RELEASE-NOTES.md b/web/modules/webform/docs/RELEASE-NOTES.md
index 0213d5ec925f40667f0549ded1177e8413536558..eb81bf7d50c25e1b5d118f7c2936671f826617df 100644
--- a/web/modules/webform/docs/RELEASE-NOTES.md
+++ b/web/modules/webform/docs/RELEASE-NOTES.md
@@ -1,57 +1,131 @@
-
 Steps for creating a new release
 --------------------------------
 
-  1. Cleanup code
-  2. Export configuration
-  3. Review code
+  1. Review code
+  2. Deprecated code
+  3. Review accessibility
   4. Run tests
   5. Generate release notes
   6. Tag and create a new release
-  7. Upload screencast to YouTube
-
-1. Cleanup code
----------------
 
-[Convert to short array syntax](https://www.drupal.org/project/short_array_syntax)
 
-    drush short-array-syntax webform
+1. Review code
+--------------
 
-Tidy YAML files
+    # Remove files that should never be reviewed.
+    cd modules/sandbox/webform
+    rm *.patch interdiff-*
+    
+[PHP](https://www.drupal.org/node/1587138)
 
-    @see DEVELOPMENT-CHEATSHEET.md
+    # Check Drupal PHP coding standards
+    cd /var/www/sites/d8_webform/web
+    phpcs --standard=Drupal --extensions=php,module,inc,install,test,profile,theme,css,info modules/sandbox/webform > ~/webform-php-coding-standards.txt
+    cat ~/webform-php-coding-standards.txt
 
+    # Check Drupal PHP best practices
+    cd /var/www/sites/d8_webform/web
+    phpcs --standard=DrupalPractice --extensions=php,module,inc,install,test,profile,theme,js,css,info modules/sandbox/webform > ~/webform-php-best-practice.txt
+    cat ~/webform-php-best-practice.txt
 
-2. Export configuration
------------------------
+[JavaScript](https://www.drupal.org/node/2873849)
 
-    @see DEVELOPMENT-CHEATSHEET.md
+    # Install Eslint. (One-time)
+    cd /var/www/sites/d8_webform/web/core
+    yarn install
+    
+    # Check Drupal JavaScript (ES5) legacy coding standards.
+    cd /var/www/sites/d8_webform/web
+    core/node_modules/.bin/eslint --no-eslintrc -c=core/.eslintrc.legacy.json --ext=.js modules/sandbox/webform > ~/webform-javascript-coding-standards.txt
+    cat ~/webform-javascript-coding-standards.txt
+          
 
+[File Permissions](https://www.drupal.org/comment/reply/2690335#comment-form)
 
-3. Review code
---------------
+    # Files should be 644 or -rw-r--r--
+    find * -type d -print0 | xargs -0 chmod 0755
 
-[Online](http://pareview.sh)
+    # Directories should be 755 or drwxr-xr-x
+    find . -type f -print0 | xargs -0 chmod 0644
 
-    http://git.drupal.org/project/webform.git 8.x-5.x
+2. Deprecated code
+------------------
+
+[phpstan-drupal](https://github.com/mglaman/phpstan-drupal) 
+[phpstan-drupal-deprecations](https://github.com/mglaman/phpstan-drupal-deprecations)
+
+    cd /var/www/sites/d8_webform/
+    composer require mglaman/phpstan-drupal
+    composer require phpstan/phpstan-deprecation-rules
+
+Create `/var/www/sites/d8_webform/phpstan.neon` 
+
+    parameters:
+      customRulesetUsed: true
+      reportUnmatchedIgnoredErrors: false
+      # Ignore phpstan-drupal extension's rules.
+      ignoreErrors:
+        - '#\Drupal calls should be avoided in classes, use dependency injection instead#'
+        - '#Plugin definitions cannot be altered.#'
+        - '#Missing cache backend declaration for performance.#'
+        - '#Plugin manager has cache backend specified but does not declare cache tags.#'
+    includes:
+      - vendor/mglaman/phpstan-drupal/extension.neon
+      - vendor/phpstan/phpstan-deprecation-rules/rules.neon
+    
+Run PHPStan with memory limit increased
 
-[Commandline](https://www.drupal.org/node/1587138)
+    cd /var/www/sites/d8_webform/
+    ./vendor/bin/phpstan --memory-limit=1024M analyse web/modules/sandbox/webform > ~/webform-deprecated.txt
+    cat ~/webform-deprecated.txt
 
-    # Check Drupal coding standards
-    phpcs --standard=Drupal --extensions=php,module,inc,install,test,profile,theme,css,info modules/sandbox/webform
     
-    # Check Drupal best practices
-    phpcs --standard=DrupalPractice --extensions=php,module,inc,install,test,profile,theme,js,css,info modules/sandbox/webform
+3. Review accessibility
+-----------------------
 
-[File Permissions](https://www.drupal.org/comment/reply/2690335#comment-form)
+[Pa11y](http://pa11y.org/)
 
-    # Files should be 644 or -rw-r--r--
-    find * -type d -print0 | xargs -0 chmod 0755
+Pa11y is your automated accessibility testing pal.
 
-    # Directories should be 755 or drwxr-xr-x
-    find . -type f -print0 | xargs -0 chmod 0644
+Notes
+- Requires node 8.x+
 
 
+    # Enable accessibility examples.
+    drush en -y webform_examples_accessibility
+    
+    # Text.
+    mkdir -p /var/www/sites/d8_webform/web/modules/sandbox/webform/reports/accessiblity/text  
+    cd /var/www/sites/d8_webform/web/modules/sandbox/webform/reports/accessiblity/text
+    pa11y http://localhost/wf/webform/example_accessibility_basic > example_accessibility_basic.txt 
+    pa11y http://localhost/wf/webform/example_accessibility_advanced > example_accessibility_advanced.txt
+    pa11y http://localhost/wf/webform/example_accessibility_containers > example_accessibility_containers.txt
+    pa11y http://localhost/wf/webform/example_accessibility_wizard > example_accessibility_wizard.txt
+    pa11y http://localhost/wf/webform/example_accessibility_labels > example_accessibility_labels.txt
+
+    # HTML.
+    mkdir -p /var/www/sites/d8_webform/web/modules/sandbox/webform/reports/accessiblity/html
+    cd /var/www/sites/d8_webform/web/modules/sandbox/webform/reports/accessiblity/html
+    pa11y --reporter html http://localhost/wf/webform/example_accessibility_basic > example_accessibility_basic.html 
+    pa11y --reporter html http://localhost/wf/webform/example_accessibility_advanced > example_accessibility_advanced.html
+    pa11y --reporter html http://localhost/wf/webform/example_accessibility_containers > example_accessibility_containers.html
+    pa11y --reporter html http://localhost/wf/webform/example_accessibility_wizard > example_accessibility_wizard.html
+    pa11y --reporter html http://localhost/wf/webform/example_accessibility_labels > example_accessibility_labels.html
+ 
+    # Remove localhost from reports.
+    cd /var/www/sites/d8_webform/web/modules/sandbox/webform/reports/accessiblity
+    find . -name '*.html' -exec sed -i '' -e  's|http://localhost/wf/webform/|http://localhost/webform/|g' {} \;
+ 
+    # PDF.
+    mkdir -p /var/www/sites/d8_webform/web/modules/sandbox/webform/reports/accessiblity/pdf
+    cd /var/www/sites/d8_webform/web/modules/sandbox/webform/reports/accessiblity/pdf
+    wkhtmltopdf --dpi 384 ../html/example_accessibility_basic.html example_accessibility_basic.pdf 
+    wkhtmltopdf --dpi 384 ../html/example_accessibility_advanced.html example_accessibility_advanced.pdf
+    wkhtmltopdf --dpi 384 ../html/example_accessibility_containers.html example_accessibility_containers.pdf
+    wkhtmltopdf --dpi 384 ../html/example_accessibility_wizard.html example_accessibility_wizard.pdf
+    wkhtmltopdf --dpi 384 ../html/example_accessibility_labels.html example_accessibility_labels.pdf
+
+    
 4. Run tests
 ------------
 
@@ -66,42 +140,43 @@ Tidy YAML files
     php core/scripts/run-tests.sh --suppress-deprecations --url http://localhost/wf --verbose --class "Drupal\Tests\webform\Functional\WebformListBuilderTest"
 
 [PHPUnit](https://www.drupal.org/node/2116263)
-     
+
 Notes
-- Links to PHP Unit HTML responses are not being printed by PHPStrom
+- Links to PHP Unit HTML responses are not being printed by PHPStorm
 
-References 
-- [Issue #2870145: Set printerClass in phpunit.xml.dist](https://www.drupal.org/node/2870145) 
+References
+- [Issue #2870145: Set printerClass in phpunit.xml.dist](https://www.drupal.org/node/2870145)
 - [Lesson 10.2 - Unit testing](https://docs.acquia.com/article/lesson-102-unit-testing)
 
+    # Export database and base URL.
+    export SIMPLETEST_DB=mysql://drupal_d8_webform:drupal.@dm1n@localhost/drupal_d8_webform;
+    export SIMPLETEST_BASE_URL='http://localhost/wf';
 
     # Execute all Webform PHPUnit tests.
-    cd /var/www/sites/d8_webform/core
-    php ../vendor/phpunit/phpunit/phpunit --printer="\Drupal\Tests\Listeners\HtmlOutputPrinter" --group webform
-
-    cd /var/www/sites/d8_webform/core
+    cd /var/www/sites/d8_webform/web/core
+    php ../../vendor/phpunit/phpunit/phpunit --printer="\Drupal\Tests\Listeners\HtmlOutputPrinter" --group webform
 
     # Execute individual PHPUnit tests.
-    export SIMPLETEST_DB=mysql://drupal_d8_webform:drupal.@dm1n@localhost/drupal_d8_webform;
-    export SIMPLETEST_BASE_URL='http://localhost/wf';
+    cd /var/www/sites/d8_webform/web/core
 
-    # Functional test.    
+    # Functional test.
     php ../../vendor/phpunit/phpunit/phpunit --printer="\Drupal\Tests\Listeners\HtmlOutputPrinter" ../modules/sandbox/webform/tests/src/Functional/WebformExampleFunctionalTest.php
 
-    # Kernal test.    
+    # Kernal test.
     php ../../vendor/phpunit/phpunit/phpunit --printer="\Drupal\Tests\Listeners\HtmlOutputPrinter" ../modules/sandbox/webform/tests/src/Kernal/Utility/WebformDialogHelperTest.php
 
     # Unit test.
-    php ../../vendor/phpunit/phpunit/phpunit --printer="\Drupal\Tests\Listeners\HtmlOutputPrinter"  ../modules/sandbox/webform/tests/src/Unit/Utility/WebformYamlTest.php
+    php ../../vendor/phpunit/phpunit/phpunit --printer="\Drupal\Tests\Listeners\HtmlOutputPrinter" ../modules/sandbox/webform/tests/src/Unit/Utility/WebformYamlTest.php
+
+    php ../../vendor/phpunit/phpunit/phpunit --printer="\Drupal\Tests\Listeners\HtmlOutputPrinter" ../modules/sandbox/webform/tests/src/Unit/Access/WebformAccessCheckTest
 
-    php ../../vendor/phpunit/phpunit/phpunit --printer="\Drupal\Tests\Listeners\HtmlOutputPrinter"  ../modules/sandbox/webform/tests/src/Unit/Access/WebformAccessCheckTest
 
 5. Generate release notes
 -------------------------
 
 [Git Release Notes for Drush](https://www.drupal.org/project/grn)
 
-    drush release-notes --nouser 8.x-5.0-VERSION 8.x-5.x
+    drush release-notes --nouser 8.x-5.0-rc1 8.x-5.x
 
 
 6. Tag and create a new release
@@ -114,11 +189,3 @@ References
     git push origin tag 8.x-5.0-VERSION
 
 [Create new release](https://www.drupal.org/node/add/project-release/2640714)
-
-
-7. Upload screencast to YouTube
--------------------------------
-
-- Title : Webform 8.x-5.x-betaXX
-- Tags: Drupal 8,Webform,Form Builder
-- Privacy: listed
diff --git a/web/modules/webform/docs/UPDATE-LIBRARIES.md b/web/modules/webform/docs/UPDATE-LIBRARIES.md
new file mode 100644
index 0000000000000000000000000000000000000000..e0816648a4df45d87c9e918fa6aae54da276c10f
--- /dev/null
+++ b/web/modules/webform/docs/UPDATE-LIBRARIES.md
@@ -0,0 +1,85 @@
+Steps for updating libraries
+----------------------------
+
+  1. Create a ticket in the Webform issue queue
+  2. Create a list of all recent releases
+  3. Update WebformLibrariesManager
+  4. Update webform.libraries.yml
+  5. Test changes
+  6. Update webform_libraries.module
+  7. Update composer.libraries.json
+
+
+1. Create a ticket in the Webform issue queue
+----------------------------------------------
+
+- https://www.drupal.org/node/add/project-issue/webform
+
+
+2. Create a list of all recent releases
+---------------------------------------
+
+- Enable all external libraries (admin/structure/webform/config/libraries)
+
+- Manually check for new releases. Only update to stable releases. 
+
+- Add list of updated external libraries to issue on Drupal.org
+
+
+3. Update WebformLibrariesManager
+---------------------------------
+
+- \Drupal\webform\WebformLibrariesManager::initLibraries
+
+
+4. Update webform.libraries.yml
+---------------------------------
+
+- webform.libraries.yml
+
+
+5. Test changes
+---------------
+
+Check external libraries are loaded from CDN.
+
+    drush webform:libraries:remove
+
+Check external libraries are download.
+
+    drush webform:libraries:download
+
+
+6. Update webform_libraries.module
+----------------------------------
+
+Enable and download all libraries
+
+    cd /var/www/sites/d8_webform
+    drush php-eval "\Drupal::configFactory()->getEditable('webform.settings')->set('libraries.excluded_libraries', [])->save();"
+    drush en -y webform_image_select
+    drush webform:libraries:download
+
+Update libraries.zip
+
+    # Remove libraries.zip.
+    rm -Rf /var/www/sites/d8_webform/web/modules/sandbox/webform_libraries/libraries.zip
+
+    # Create libraries.zip
+    cd /var/www/sites/d8_webform/web/
+    zip -r libraries.zip libraries
+    mv libraries.zip /private/var/www/sites/d8_webform/web/modules/sandbox/webform_libraries/libraries.zip
+
+Commit changes
+
+    # Commit changes.
+    cd /private/var/www/sites/d8_webform/web/modules/sandbox/webform_libraries/
+    git commit -am"Update webform_libraries"
+    git push
+
+
+7. Update composer.libraries.json
+----------------------------------
+
+    cd /private/var/www/sites/d8_webform/web/modules/sandbox/webform
+    drush webform:libraries:composer > composer.libraries.json
diff --git a/web/modules/webform/drush/webform.drush.inc b/web/modules/webform/drush/webform.drush.inc
index b1b698eb5f7e914e95be14110028c6f302dbe088..9c3525d62bf88a7d73cd9eef5f7f061d2cc1c366 100644
--- a/web/modules/webform/drush/webform.drush.inc
+++ b/web/modules/webform/drush/webform.drush.inc
@@ -24,6 +24,7 @@ function webform_drush_command() {
         'webform' => 'The webform ID you want to export (required unless --entity-type and --entity-id are specified)',
       ),
       'options' => array(
+        'exporter' => 'The type of export. (delimited, table, yaml, or json)',
         'delimiter' => 'Delimiter between columns (defaults to site-wide setting). This option may need to be wrapped in quotes. i.e. --delimiter="\t".',
         'multiple-delimiter' => 'Delimiter between an element with multiple values (defaults to site-wide setting).',
         'file-name' => 'File name used to export submission and uploaded filed. You may use tokens.',
@@ -33,6 +34,7 @@ function webform_drush_command() {
         'options-multiple-format' => 'Set to "separate" (default) or "compact" to determine how multiple select list values are exported.',
         'entity-reference-items' => 'Comma-separated list of entity reference items (id, title, and/or url) to be exported.',
         'excluded-columns' => 'Comma-separated list of component IDs or webform keys to exclude.',
+        'uuid' => ' Use UUIDs for all entity references. (Only applies to CSV download)',
         'entity-type' => 'The entity type to which this submission was submitted from.',
         'entity-id' => 'The ID of the entity of which this webform submission was submitted from.',
         'range-type' => 'Range of submissions to export: "all", "latest", "serial", "sid", or "date".',
@@ -43,12 +45,32 @@ function webform_drush_command() {
         'state' => 'Submission state to be included: "completed", "draft" or "all" (default).',
         'sticky' => 'Flagged/starred submission status.',
         'files' => 'Download files: "1" or "0" (default). If set to 1, the exported CSV file and any submission file uploads will be download in a gzipped tar file.',
-        'destination' => 'The full path and filename in which the CSV or archive should be stored. If omitted the CSV file or archive will be outputted to the commandline.',
+        'destination' => 'The full path and filename in which the CSV or archive should be stored. If omitted the CSV file or archive will be outputted to the command line.',
       ),
       'aliases' => array(
         'wfx',
       ),
     ),
+    'webform-import' => array(
+      'description' => 'Imports webform submissions from a CSV file.',
+      'core' => array(
+        '8+',
+      ),
+      'bootstrap' => 2,
+      'arguments' => array(
+        'webform' => 'The webform ID you want to import (required unless --entity-type and --entity-id are specified)',
+        'import_uri' => 'The path or URI for the CSV file to be imported.',
+      ),
+      'options' => array(
+        'skip_validation' => 'Skip form validation.',
+        'treat_warnings_as_errors' => 'Treat all warnings as errors.',
+        'entity-type' => 'The entity type to which this submission was submitted from.',
+        'entity-id' => 'The ID of the entity of which this webform submission was submitted from.',
+      ),
+      'aliases' => array(
+        'wfi',
+      ),
+    ),
     'webform-purge' => array(
       'description' => 'Purge webform submissions from the databases',
       'core' => array(
@@ -177,13 +199,13 @@ function webform_drush_command() {
       ),
     ),
     'webform-repair' => array(
-      'description' => 'Makes sure all Webform admin settings and webforms are up-to-date.',
+      'description' => 'Makes sure all Webform admin configuration and webform settings are up-to-date.',
       'core' => array(
         '8+',
       ),
       'bootstrap' => 1,
       'examples' => array(
-        'webform-repair' => 'Repairs admin settings and webforms are up-to-date.',
+        'webform-repair' => 'Repairs admin configuration and webform settings are up-to-date.',
       ),
       'aliases' => array(
         'wfr',
@@ -252,6 +274,24 @@ function drush_webform_export() {
   return call_user_func_array([\Drupal::service('webform.cli_service'), 'drush_webform_export'], func_get_args());
 }
 
+/******************************************************************************/
+// drush webform-import. DO NOT EDIT.
+/******************************************************************************/
+
+/**
+ * Implements drush_hook_COMMAND_validate().
+ */
+function drush_webform_import_validate() {
+  return call_user_func_array([\Drupal::service('webform.cli_service'), 'drush_webform_import_validate'], func_get_args());
+}
+
+/**
+ * Implements drush_hook_COMMAND().
+ */
+function drush_webform_import() {
+  return call_user_func_array([\Drupal::service('webform.cli_service'), 'drush_webform_import'], func_get_args());
+}
+
 /******************************************************************************/
 // drush webform-purge. DO NOT EDIT.
 /******************************************************************************/
diff --git a/web/modules/webform/images/elements/date-calendar.png b/web/modules/webform/images/elements/date-calendar.png
new file mode 100644
index 0000000000000000000000000000000000000000..60c2f3c15b18475a92179b57acc53cea565d5f25
--- /dev/null
+++ b/web/modules/webform/images/elements/date-calendar.png
@@ -0,0 +1,3 @@
+�PNG
+
+���
IHDR����������Xo ���tEXtSoftware�Adobe ImageReadyq�e<���PLTE���u��N��/��1�ɴ��qqq������s��-���	tRNS���������SOx���8IDATx�b���lh�$���L �!F��T�����Y�.!V@��~����R�J>w#O����IEND�B`�
\ No newline at end of file
diff --git a/web/modules/webform/includes/webform.date.inc b/web/modules/webform/includes/webform.date.inc
index 4c2f32e6d9bbbc34b936286007758ee36a02fb7c..db63b428a969f08f5b7620f9762ccf78c773a1d2 100644
--- a/web/modules/webform/includes/webform.date.inc
+++ b/web/modules/webform/includes/webform.date.inc
@@ -7,9 +7,31 @@
 
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Datetime\DateHelper;
 
 /**
- * Callback for custom datetime datepicker.
+ * Callback for removing abbreviation from datelist.
+ *
+ * @param array $element
+ *   The element.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ *   The form state.
+ * @param \Drupal\Core\Datetime\DrupalDateTime|null $date
+ *   The date value.
+ *
+ * @see \Drupal\Core\Datetime\Element\Datelist::processDatelist
+ */
+function _webform_datelist_date_date_callback(array &$element, FormStateInterface $form_state, $date) {
+  $no_abbreviate = (isset($element['#date_abbreviate']) && $element['#date_abbreviate'] === FALSE);
+  if ($no_abbreviate && isset($element['month']) && isset($element['month']['#options'])) {
+    // Load translated date part labels from the appropriate calendar plugin.
+    $date_helper = new DateHelper();
+    $element['month']['#options'] = $date_helper->monthNames($element['#required']);
+  }
+}
+
+/**
+ * Callback for custom datetime date element.
  *
  * @param array $element
  *   The element.
@@ -20,19 +42,27 @@
  *
  * @see \Drupal\webform\Plugin\WebformElement\DateTime::prepare
  */
-function _webform_datetime_datepicker(array &$element, FormStateInterface $form_state, DrupalDateTime $date = NULL) {
-  // Convert #type from datepicker to textfield.
-  if (isset($element['#date_date_element']) && $element['#date_date_element'] === 'datepicker') {
-    $element['date']['#type'] = 'textfield';
+function _webform_datetime_date(array &$element, FormStateInterface $form_state, DrupalDateTime $date = NULL) {
+  // Make sure the date element is being displayed.
+  if (!isset($element['date'])) {
+    return;
+  }
+
+  $type = (isset($element['#date_date_element'])) ? $element['#date_date_element'] : 'date';
+  switch ($type) {
+    case 'datepicker':
+      // Convert #type from datepicker to textfield.
+      $element['date']['#type'] = 'textfield';
 
-    // Must manually set 'data-drupal-date-format' to trigger date picker.
-    // @see \Drupal\Core\Render\Element\Date::processDate
-    $element['date']['#attributes']['data-drupal-date-format'] = [$element['date']['#date_date_format']];
+      // Must manually set 'data-drupal-date-format' to trigger date picker.
+      // @see \Drupal\Core\Render\Element\Date::processDate
+      $element['date']['#attributes']['data-drupal-date-format'] = [$element['date']['#date_date_format']];
+      break;
   }
 }
 
 /**
- * Callback for custom datetime timepicker.
+ * Callback for custom datetime time element.
  *
  * @param array $element
  *   The element.
@@ -43,16 +73,58 @@ function _webform_datetime_datepicker(array &$element, FormStateInterface $form_
  *
  * @see \Drupal\webform\Plugin\WebformElement\DateTime::prepare
  */
-function _webform_datetime_timepicker(array &$element, FormStateInterface $form_state, DrupalDateTime $date = NULL) {
-  // Convert #type from timepicker to textfield.
-  if (isset($element['#date_time_element']) && $element['#date_time_element'] === 'timepicker') {
-    $element['time']['#type'] = 'textfield';
+function _webform_datetime_time(array &$element, FormStateInterface $form_state, DrupalDateTime $date = NULL) {
+  // Make sure the time element is being displayed.
+  if (!isset($element['time'])) {
+    return;
+  }
+
+  // Apply time specific min/max to the element.
+  foreach (['min', 'max'] as $property) {
+    if (!empty($element["#date_time_$property"])) {
+      $value = $element["#date_time_$property"];
+    }
+    elseif (!empty($element["#date_$property"])) {
+      $value = date('H:i:s', strtotime($element["#date_$property"]));
+    }
+    else {
+      $value = NULL;
+    }
+    if ($value) {
+      $element['time']["#$property"] = $value;
+      $element['time']['#attributes'][$property] = $value;
+    }
+  }
+
+  // Apply time step and format to the element.
+  if (!empty($element['#date_time_step'])) {
+    $element['time']['#step'] = $element['#date_time_step'];
+    $element['time']['#attributes']['step'] = $element['#date_time_step'];
+  }
+  if (!empty($element['#date_time_format'])) {
+    $element['time']['#time_format'] = $element['#date_time_format'];
   }
 
-  // Must manually set 'data-webform-time-format' to trigger time picker.
-  // @see \Drupal\webform\Element\WebformTime::processWebformTime
-  $element['time']['#attributes']['data-webform-time-format'] = [$element['#date_time_format']];
+  // Remove extra attributes for date element.
+  unset(
+    $element['time']['#attributes']['data-min-year'],
+    $element['time']['#attributes']['data-max-year']
+  );
+
+  $type = (isset($element['#date_time_element'])) ? $element['#date_time_element'] : 'time';
 
-  // Attached custom timepicker library.
-  $element['time']['#attached']['library'][] = 'webform/webform.element.time';
+  switch ($type) {
+    case 'timepicker':
+      $element['time']['#type'] = 'webform_time';
+      $element['time']['#timepicker'] = TRUE;
+      break;
+
+    case 'time':
+      $element['time']['#type'] = 'webform_time';
+      break;
+
+    case 'text':
+      $element['time']['#element_validate'][] = ['\Drupal\webform\Element\WebformTime', 'validateWebformTime'];
+      break;
+  }
 }
diff --git a/web/modules/webform/includes/webform.editor.inc b/web/modules/webform/includes/webform.editor.inc
new file mode 100644
index 0000000000000000000000000000000000000000..e0ed7364401bec0134b661897cd4fd0bcdc8f19f
--- /dev/null
+++ b/web/modules/webform/includes/webform.editor.inc
@@ -0,0 +1,267 @@
+<?php
+
+/**
+ * @file
+ * Webform module editor file upload hooks.
+ *
+ * Because Webforms are config entities the editor.module's file uploading
+ * is not supported.
+ *
+ * The below code adds file upload support to Webform config entities and
+ * 'webform.settings' config.
+ *
+ * Below functions are copied from editor.module.
+ */
+
+use Drupal\Core\Config\Config;
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Serialization\Yaml;
+use Drupal\webform\WebformInterface;
+use Drupal\Component\Utility\Html;
+
+/******************************************************************************/
+// Webform entity hooks.
+/******************************************************************************/
+
+/**
+ * Implements hook_webform_insert().
+ *
+ * @see editor_entity_insert()
+ */
+function webform_webform_insert(WebformInterface $webform) {
+  $uuids = _webform_get_config_entity_file_uuids($webform);
+  _webform_record_file_usage($uuids, $webform->getEntityTypeId(), $webform->id());
+}
+
+/**
+ * Implements hook_webform_update().
+ *
+ * @see editor_entity_update()
+ */
+function webform_webform_update(WebformInterface $webform) {
+  $original_uuids = _webform_get_config_entity_file_uuids($webform->original);
+  $uuids = _webform_get_config_entity_file_uuids($webform);
+
+  // Detect file usages that should be incremented.
+  $added_files = array_diff($uuids, $original_uuids);
+  _webform_record_file_usage($added_files, $webform->getEntityTypeId(), $webform->id());
+
+  // Detect file usages that should be decremented.
+  $removed_files = array_diff($original_uuids, $uuids);
+  _webform_delete_file_usage($removed_files, $webform->getEntityTypeId(), $webform->id(), 1);
+}
+
+/**
+ * Implements hook_webform_delete().
+ *
+ * @see editor_entity_delete()
+ */
+function webform_webform_delete(WebformInterface $webform) {
+  $uuids = _webform_get_config_entity_file_uuids($webform);
+  _webform_delete_file_usage($uuids, $webform->getEntityTypeId(), $webform->id(), 0);
+}
+
+/******************************************************************************/
+// Webform config (settings) hooks.
+// @see \Drupal\webform\Form\AdminConfig\WebformAdminConfigBaseForm::loadConfig
+// @see \Drupal\webform\Form\AdminConfig\WebformAdminConfigBaseForm::saveConfig
+/******************************************************************************/
+
+/**
+ * Update config editor file references.
+ *
+ * @param \Drupal\Core\Config\Config $config
+ *   An editable configuration object.
+ */
+function _webform_config_update(Config $config) {
+  $original_uuids = _webform_get_array_file_uuids($config->getOriginal());
+  $uuids = _webform_get_array_file_uuids($config->getRawData());
+
+  // Detect file usages that should be incremented.
+  $added_files = array_diff($uuids, $original_uuids);
+  _webform_record_file_usage($added_files, 'config', $config->getName());
+
+  // Detect file usages that should be decremented.
+  $removed_files = array_diff($original_uuids, $uuids);
+  _webform_delete_file_usage($removed_files, 'config', $config->getName(), 1);
+}
+
+/**
+ * Delete config editor file references.
+ *
+ * @param \Drupal\Core\Config\Config $config
+ *   An editable configuration object.
+ *
+ * @see webform_uninstall()
+ */
+function _webform_config_delete(Config $config) {
+  $uuids = _webform_get_array_file_uuids($config->getRawData());
+  _webform_delete_file_usage($uuids, 'config', $config->getName(), 0);
+}
+
+/******************************************************************************/
+// Config entity functions.
+/******************************************************************************/
+
+/**
+ * Finds all files referenced (data-entity-uuid) by config entity.
+ *
+ * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
+ *   An entity whose fields to analyze.
+ *
+ * @return array
+ *   An array of file entity UUIDs.
+ *
+ * @see _editor_get_file_uuids_by_field()
+ */
+function _webform_get_config_entity_file_uuids(ConfigEntityInterface $entity) {
+  return _webform_get_array_file_uuids($entity->toArray());
+}
+
+/******************************************************************************/
+// Config settings functions.
+/******************************************************************************/
+
+/**
+ * Finds all files referenced (data-entity-uuid) in an associatve array.
+ *
+ * @param array $data
+ *   An associative array.
+ *
+ * @return array
+ *   An array of file entity UUIDs.
+ *
+ * @see _editor_get_file_uuids_by_field()
+ */
+function _webform_get_array_file_uuids(array $data) {
+  $text = Yaml::encode($data);
+  return _webform_parse_file_uuids($text);
+}
+
+/******************************************************************************/
+// File usage functions.
+/******************************************************************************/
+
+/**
+ * Records file usage of files referenced by formatted text fields.
+ *
+ * Every referenced file that does not yet have the FILE_STATUS_PERMANENT state,
+ * will be given that state.
+ *
+ * @param array $uuids
+ *   An array of file entity UUIDs.
+ * @param string $type
+ *   The type of the object that contains the referenced file.
+ * @param string $id
+ *   The unique ID of the object containing the referenced file.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ *
+ * @see _editor_record_file_usage()
+ */
+function _webform_record_file_usage(array $uuids, $type, $id) {
+  if (empty($uuids) || !\Drupal::moduleHandler()->moduleExists('file')) {
+    return;
+  }
+
+  /** @var \Drupal\file\FileUsage\FileUsageInterface $file_usage */
+  $file_usage = \Drupal::service('file.usage');
+
+  foreach ($uuids as $uuid) {
+    if ($file = \Drupal::entityManager()->loadEntityByUuid('file', $uuid)) {
+      if ($file->status !== FILE_STATUS_PERMANENT) {
+        $file->status = FILE_STATUS_PERMANENT;
+        $file->save();
+      }
+      $file_usage->add($file, 'editor', $type, $id);
+    }
+  }
+}
+
+/**
+ * Deletes file usage of files referenced by formatted text fields.
+ *
+ * @param array $uuids
+ *   An array of file entity UUIDs.
+ * @param string $type
+ *   The type of the object that contains the referenced file.
+ * @param string $id
+ *   The unique ID of the object containing the referenced file.
+ * @param int $count
+ *   The number of references to delete. Should be 1 when deleting a single
+ *   revision and 0 when deleting an entity entirely.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ *
+ * @see \Drupal\file\FileUsage\FileUsageInterface::delete()
+ * @see _editor_delete_file_usage()
+ */
+function _webform_delete_file_usage(array $uuids, $type, $id, $count) {
+  if (empty($uuids) || !\Drupal::moduleHandler()->moduleExists('file')) {
+    return;
+  }
+
+  /** @var \Drupal\file\FileUsage\FileUsageInterface $file_usage */
+  $file_usage = \Drupal::service('file.usage');
+
+  $make_unused_managed_files_temporary = \Drupal::config('webform.settings')->get('html_editor.make_unused_managed_files_temporary');
+  foreach ($uuids as $uuid) {
+    if ($file = \Drupal::entityManager()->loadEntityByUuid('file', $uuid)) {
+      $file_usage->delete($file, 'editor', $type, $id, $count);
+      // Make unused files temporary.
+      if ($make_unused_managed_files_temporary && empty($file_usage->listUsage($file)) && !$file->isTemporary()) {
+        $file->setTemporary();
+        $file->save();
+      }
+    }
+  }
+}
+
+/******************************************************************************/
+// File parsing functions.
+/******************************************************************************/
+
+/**
+ * Parse an HTML snippet for any linked file with data-entity-uuid attributes.
+ *
+ * @param string $text
+ *   The partial (X)HTML snippet to load. Invalid markup will be corrected on
+ *   import.
+ *
+ * @return array
+ *   An array of all found UUIDs.
+ *
+ * @see _editor_parse_file_uuids()
+ */
+function _webform_parse_file_uuids($text) {
+  if (strpos($text, 'data-entity-uuid') === FALSE) {
+    return [];
+  }
+
+  $uuids = [];
+  // Get all images using a regex.
+  if (preg_match_all('/<img[^>]+>/', $text, $matches)) {
+    foreach ($matches[0] as $img) {
+      // Cleanup quotes escaped via YAML.
+      // Please note, calling stripslashes() twice because elements are
+      // double escaped.
+      $img = stripslashes(stripslashes($img));
+
+      // Create a DomElement so that we can parse the image's attributes.
+      $dom_node = Html::load($img)->getElementsByTagName('img')->item(0);
+
+      // Get the entity type and uuid.
+      $type = $dom_node->getAttribute('data-entity-type');
+      $uuid = $dom_node->getAttribute('data-entity-uuid');
+
+      // Check the image is a file entity with a uuid.
+      if ($type === 'file' && $uuid) {
+        $uuids[] = $uuid;
+      }
+    }
+  }
+
+  // Use array_unique() to collect one uuid per uploaded file.
+  // This prevents cut-n-pasted uploaded files from having multiple usages.
+  return array_unique($uuids);
+}
diff --git a/web/modules/webform/includes/webform.install.inc b/web/modules/webform/includes/webform.install.inc
index 75a774d1394f6d124eccfb2cf6e22b1da8ba9379..abc65034408ca968a35767c4e5fc9af70b384209 100644
--- a/web/modules/webform/includes/webform.install.inc
+++ b/web/modules/webform/includes/webform.install.inc
@@ -18,7 +18,7 @@
  * done via an update hook.
  *
  * @param bool $reset
- *   If set TRUE old admin settings will be completly deleted.
+ *   If set TRUE old admin settings will be completely deleted.
  *
  * @see drush_webform_repair()
  */
@@ -68,7 +68,7 @@ function _webform_update_admin_settings($reset = FALSE) {
 }
 
 /**
- * Update webform setting to reflect changes in the default settings.
+ * Update webform settings to reflect changes in the default settings.
  *
  * This function can be used to apply new webform settings to all existing
  * webforms.
@@ -76,14 +76,35 @@ function _webform_update_admin_settings($reset = FALSE) {
  * @see \Drupal\webform\Entity\Webform::setSettings
  */
 function _webform_update_webform_settings() {
+  $config_factory = \Drupal::configFactory();
+  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
+    $webform_config = $config_factory->getEditable($webform_config_name);
+    $data = $webform_config->getRawData();
+    $data = _webform_update_webform_setting($data);
+    $webform_config->setData($data)->save();
+  }
+}
+
+/**
+ * Update webform setting to reflect changes in the default settings.
+ *
+ * @param array $data
+ *   A webform's raw configuration data from webform.webform.*.yml.
+ *
+ * @return array
+ *   Updated raw configuration data.
+ */
+function _webform_update_webform_setting(array $data) {
   $default_properties = [
     'langcode' => 'en',
     'status' => WebformInterface::STATUS_OPEN,
     'dependencies' => [],
     'open' => NULL,
     'close' => NULL,
+    'weight' => 0,
     'uid' => '',
     'template' => FALSE,
+    'archive' => FALSE,
     'id' => '',
     'title' => '',
     'description' => '',
@@ -97,79 +118,82 @@ function _webform_update_webform_settings() {
   ];
 
   $default_settings = Webform::getDefaultSettings();
-  $config_factory = \Drupal::configFactory();
-  // Update 'webform.webform.*' configuration.
-  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
-    $webform_config = $config_factory->getEditable($webform_config_name);
 
-    // Get data.
-    $data = $webform_config->getRawData();
-
-    // Always apply the default properties.
-    $properties = $default_properties;
-    // Now apply defined properties.
-    foreach ($data as $name => $value) {
-      $properties[$name] = $value;
-    }
-    // Set properties.
-    $data = $properties;
+  // Always apply the default properties.
+  $properties = $default_properties;
+  // Now apply defined properties.
+  foreach ($data as $name => $value) {
+    $properties[$name] = $value;
+  }
+  // Set properties.
+  $data = $properties;
 
-    // Always apply the default settings.
-    $settings = $default_settings;
-    // Now apply custom settings.
-    foreach ($data['settings'] as $name => $value) {
-      $settings[$name] = $value;
-    }
-    // Set settings.
-    $data['settings'] = $settings;
+  // Always apply the default settings.
+  $settings = $default_settings;
+  // Now apply custom settings.
+  foreach ($data['settings'] as $name => $value) {
+    $settings[$name] = $value;
+  }
+  // Set settings.
+  $data['settings'] = $settings;
 
-    // Set access.
-    $data['access'] += Webform::getDefaultAccessRules();
+  // Set access.
+  /** @var \Drupal\webform\WebformAccessRulesManagerInterface $access_rules_manager */
+  $access_rules_manager = \Drupal::service('webform.access_rules_manager');
+  $data['access'] += $access_rules_manager->getDefaultAccessRules();
 
-    // Save data.
-    $webform_config->setData($data)->save();
-  }
+  return $data;
 }
 
 /**
- * Update webform handler configuration and settings to reflect changes in a handler's default configuration.
- *
- * Must be called by passing in the handler's class using ::class.
- *
- * Example usage:
- * @code
- * _webform_update_webform_handler_configuration();
- * _webform_update_webform_handler_configuration(EmailWebformHandler::class);
- * @endcode
- *
- * @param string $handler_class
- *   A handler class instance. If NULL all handlers will be updated.
- *   Defaults to NULL.
+ * Update webform handler settings to reflect changes in a handler's default configuration.
  *
  * @see \Drupal\webform\Plugin\WebformHandlerInterface
  */
-function _webform_update_webform_handler_configuration($handler_class = NULL) {
-  /** @var \Drupal\webform\WebformInterface[] $webforms */
-  $webforms = Webform::loadMultiple();
-  foreach ($webforms as $webform) {
+function _webform_update_webform_handler_settings() {
+  // Issue #2863986: Allow updating modules with new service dependencies.
+  \Drupal::service('kernel')->rebuildContainer();
+
+  // Get the default configuration (aka settings) for all handlers provided
+  // by the Webform module.
+  /** @var \Drupal\webform\Plugin\WebformHandlerManagerInterface $handler_manager */
+  $handler_manager = \Drupal::service('plugin.manager.webform.handler');
+  $definitions = $handler_manager->getDefinitions();
+  $default_handler_settings = [];
+  foreach ($definitions as $plugin_id => $definition) {
+    if (strpos($definition['provider'], 'webform_test_') === 0 || in_array($definition['provider'], ['webform', 'webform_scheduled_email'])) {
+      $default_handler_settings[$plugin_id] = $handler_manager->createInstance($plugin_id)->defaultConfiguration();
+    }
+  }
+
+  $config_factory = \Drupal::configFactory();
+  // Update 'webform.webform.*' configuration.
+  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
+    $webform_config = $config_factory->getEditable($webform_config_name);
+
+    // Get data.
+    $data = $webform_config->getRawData();
+
+    // Apply the default handler settings.
     $has_handler = FALSE;
-    $handlers = $webform->getHandlers();
-    foreach ($handlers as $handler) {
-      $is_handler_class = ($handler_class === NULL || $handler instanceof $handler_class);
-      if ($is_handler_class) {
-        $has_handler = TRUE;
-        $configuration = $handler->getConfiguration();
+    foreach ($data['handlers'] as &$handler) {
+      if (!isset($default_handler_settings[$handler['id']])) {
+        continue;
+      }
 
-        $settings = $handler->defaultConfiguration();
-        foreach ($configuration['settings'] as $settings_key => $setting_value) {
-          $settings[$settings_key] = $setting_value;
-        }
-        $configuration['settings'] = $settings;
-        $handler->setConfiguration($configuration);
+      $settings = $default_handler_settings[$handler['id']];
+      foreach ($handler['settings'] as $settings_key => $setting_value) {
+        $settings[$settings_key] = $setting_value;
+      }
+
+      if ($handler['settings'] != $settings) {
+        $has_handler = TRUE;
+        $handler['settings'] = $settings;
       }
     }
+
     if ($has_handler) {
-      $webform->save();
+      $webform_config->setData($data)->save();
     }
   }
 }
@@ -261,6 +285,38 @@ function _webform_update_webform_submission_storage_schema() {
   $manager->updateEntityType($manager->getEntityType('webform_submission'));
 }
 
+/**
+ * Replace string in webform.settings.yml and webform.webform.*.yml.
+ *
+ * @param string $search
+ *   String to be search for.
+ * @param string $replace
+ *   String to be replace with.
+ */
+function _webform_update_string_replace($search, $replace) {
+  $config_factory = \Drupal::configFactory();
+
+  // Update 'webform.settings' configuration.
+  $settings_config = \Drupal::configFactory()->getEditable('webform.settings');
+  $yaml = Yaml::encode($settings_config->getRawData());
+  if (strpos($yaml, $search) !== FALSE) {
+    $yaml = str_replace($search, $replace, $yaml);
+    $settings_config->setData(Yaml::decode($yaml));
+    $settings_config->save();
+  }
+
+  // Update 'webform.webform.*' configuration.
+  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
+    $webform_config = $config_factory->getEditable($webform_config_name);
+    $yaml = Yaml::encode($webform_config->getRawData());
+    if (strpos($yaml, $search) !== FALSE) {
+      $yaml = str_replace($search, $replace, $yaml);
+      $webform_config->setData(Yaml::decode($yaml));
+      $webform_config->save();
+    }
+  }
+}
+
 /**
  * Clear/remove selected webform element properties.
  *
@@ -308,4 +364,3 @@ function _webform_update_elements_clear_properties_recursive(array &$element, ar
     }
   }
 }
-
diff --git a/web/modules/webform/includes/webform.install.requirements.inc b/web/modules/webform/includes/webform.install.requirements.inc
index d7ba827d8ed747a533ab6ef06127153c1db62368..c8ce139862cacb3bc5561eb2e4fd85f825f7e5c6 100644
--- a/web/modules/webform/includes/webform.install.requirements.inc
+++ b/web/modules/webform/includes/webform.install.requirements.inc
@@ -23,7 +23,7 @@ function webform_requirements($phase) {
   // Check HTML email handling.
   /****************************************************************************/
 
-  /** @var \Drupal\webform\WebformEmailProviderInterface $email_provider */
+  /** @var \Drupal\webform\WebformEProviderInterface $email_provider */
   $email_provider = \Drupal::service('webform.email_provider');
   $mail_module_name = $email_provider->getModuleName();
   $mail_plugin_id = $email_provider->getMailPluginId();
diff --git a/web/modules/webform/includes/webform.install.update.inc b/web/modules/webform/includes/webform.install.update.inc
index 00942f68b0af6cf63e1d83438641f78c1a4d58ef..9df1f4d580229af11116ba23105f93425980faae 100644
--- a/web/modules/webform/includes/webform.install.update.inc
+++ b/web/modules/webform/includes/webform.install.update.inc
@@ -5,22 +5,38 @@
  * Archived Webform update hooks.
  */
 
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\Component\Uuid\Php as Uuid;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Database\Database;
 use Drupal\Core\Serialization\Yaml;
 use Drupal\Core\Render\Element;
+use Drupal\Core\Url;
+use Drupal\views\Entity\View;
 use Drupal\webform\Entity\Webform;
 use Drupal\webform\Entity\WebformOptions;
-use Drupal\webform\Plugin\WebformHandler\ActionWebformHandler;
 use Drupal\webform\Plugin\WebformHandler\EmailWebformHandler;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\Plugin\WebformHandler\RemotePostWebformHandler;
 use Drupal\webform\Utility\WebformArrayHelper;
+use Drupal\webform\Utility\WebformFormHelper;
 use Drupal\webform\Utility\WebformOptionsHelper;
 use Drupal\webform\Utility\WebformReflectionHelper;
 use Drupal\webform\Utility\WebformYaml;
 
+/**
+ * Implements hook_update_dependencies().
+ */
+function webform_update_dependencies() {
+  // Ensure that system_update_8501() runs before the webform update, so that
+  // the new revision_default field is installed in the correct table.
+  // @see https://www.drupal.org/project/webform/issues/2958102
+  $dependencies['webform'][8099]['system'] = 8501;
+
+  return $dependencies;
+}
+
 /******************************************************************************/
 // Webform-8.x-5.0-beta1 - December 7, 2016 (No update required).
 /******************************************************************************/
@@ -62,23 +78,7 @@ function webform_update_8001() {
  * Issue #2834572: Refactor and improve token management.
  */
 function webform_update_8002() {
-  $config_factory = \Drupal::configFactory();
-
-  // Update 'webform.settings' configuration.
-  $settings_config = \Drupal::configFactory()->getEditable('webform.settings');
-  $yaml = Yaml::encode($settings_config->getRawData());
-  $yaml = str_replace('[webform_submission:', '[webform_submission:', $yaml);
-  $settings_config->setData(Yaml::decode($yaml));
-  $settings_config->save();
-
-  // Update 'webform.webform.*' configuration.
-  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
-    $webform_config = $config_factory->getEditable($webform_config_name);
-    $yaml = Yaml::encode($webform_config->getRawData());
-    $yaml = str_replace('[webform_submission:', '[webform_submission:', $yaml);
-    $webform_config->setData(Yaml::decode($yaml));
-    $webform_config->save();
-  }
+  _webform_update_string_replace('[webform-submission:', '[webform_submission:');
 }
 
 /**
@@ -244,6 +244,9 @@ function webform_update_8011() {
  *   A form element.
  */
 function _webform_update_8011(array &$element) {
+  // Issue #2863986: Allow updating modules with new service dependencies.
+  \Drupal::service('kernel')->rebuildContainer();
+
   if (isset($element['#format'])) {
     /** @var \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager */
     $element_manager = \Drupal::service('plugin.manager.webform.element');
@@ -313,6 +316,9 @@ function webform_update_8014() {
  *   A form element.
  */
 function _webform_update_8014(array &$element) {
+  // Issue #2863986: Allow updating modules with new service dependencies.
+  \Drupal::service('kernel')->rebuildContainer();
+
   /** @var \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager */
   $element_manager = \Drupal::service('plugin.manager.webform.element');
 
@@ -490,6 +496,9 @@ function webform_update_8024() {
  * Issue #2857417: Add support for open and close date/time to Webform nodes. Update database scheme.
  */
 function webform_update_8025() {
+  // Issue #2863986: Allow updating modules with new service dependencies.
+  \Drupal::service('kernel')->rebuildContainer();
+
   /** @var \Drupal\webform\WebformEntityReferenceManagerInterface $entity_reference_manager */
   $entity_reference_manager = \Drupal::service('webform.entity_reference_manager');
 
@@ -628,7 +637,7 @@ function webform_update_8030() {
  * Issue #2854021: Send email based on element options selection.
  */
 function webform_update_8031() {
-  _webform_update_webform_handler_configuration(EmailWebformHandler::class);
+  _webform_update_webform_handler_settings();
 }
 
 /**
@@ -733,7 +742,7 @@ function webform_update_8034() {
 /******************************************************************************/
 
 /******************************************************************************/
-// Webform-8.x-5.0-beta12 -  April 21, 2017.
+// Webform-8.x-5.0-beta12 - April 21, 2017.
 /******************************************************************************/
 
 /**
@@ -1012,7 +1021,7 @@ function webform_update_8046() {
  *   Webform config data.
  *
  * @return array
- *   Webform config data with 'webfor_actions' element.
+ *   Webform config data with 'webform_actions' element.
  */
 function _webform_update_8046_convert_data(array $data) {
   $button_names = [
@@ -1196,7 +1205,7 @@ function webform_update_8057() {
 }
 
 /**
- * Issue #2888076: Redirect users to login page when trying to access the a protected webform file.
+ * Issue #2888076: Redirect users to login page when trying to access a protected webform file.
  */
 function webform_update_8058() {
   _webform_update_admin_settings();
@@ -1583,7 +1592,7 @@ function webform_update_8077() {
  */
 function webform_update_8078() {
   _webform_update_admin_settings();
-  _webform_update_webform_handler_configuration(EmailWebformHandler::class);
+  _webform_update_webform_handler_settings();
 }
 
 /**
@@ -1613,7 +1622,7 @@ function webform_update_8080() {
  * Issue #2913215: Remote Post handler add GET method support.
  */
 function webform_update_8081() {
-  _webform_update_webform_handler_configuration(RemotePostWebformHandler::class);
+  _webform_update_webform_handler_settings();
 }
 
 /**
@@ -1826,7 +1835,7 @@ function webform_update_8096() {
  * Issue #2932607: Add Twig support to email body.
  */
 function webform_update_8097() {
-  _webform_update_webform_handler_configuration(EmailWebformHandler::class);
+  _webform_update_webform_handler_settings();
 }
 
 /******************************************************************************/
@@ -1891,7 +1900,7 @@ function webform_update_8099() {
  */
 function webform_update_8100() {
   // Add locked to action handler.
-  _webform_update_webform_handler_configuration(ActionWebformHandler::class);
+  _webform_update_webform_handler_settings();
 
   // Add locked to remote post handler's excluded data.
   /** @var \Drupal\webform\WebformInterface[] $webforms */
@@ -1973,7 +1982,7 @@ function webform_update_8105() {
 }
 
 /******************************************************************************/
-// Webform-8.x-5.0-rc4
+// Webform-8.x-5.0-rc4 - March 13, 2018.
 /******************************************************************************/
 
 /**
@@ -2048,7 +2057,6 @@ function webform_update_8110() {
     $excluded_elements['password_confirm'] = 'password_confirm';
     $admin_config->set('element.excluded_elements', $excluded_elements);
     $admin_config->save();
-
   }
 }
 
@@ -2087,7 +2095,6 @@ function webform_update_8113() {
   $config->save();
 }
 
-
 /**
  * Issue #2951921: The contribute module is missing from the file system.
  */
@@ -2122,6 +2129,18 @@ function webform_update_8114() {
     ->execute();
 }
 
+/******************************************************************************/
+// Webform-8.x-5.0-rc5 - March 14, 2018 (No update required).
+/******************************************************************************/
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc6 - March 16, 2018 (No update required).
+/******************************************************************************/
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc7 - March 23, 2018.
+/******************************************************************************/
+
 /**
  * Issue #2955218: Allow query and token to be removed from confirmation URL.
  */
@@ -2129,8 +2148,12 @@ function webform_update_8115() {
   _webform_update_webform_settings();
 }
 
+/******************************************************************************/
+// Webform-8.x-5.0-rc8 - April 2, 2018.
+/******************************************************************************/
+
 /**
- * #multiple__label and #multiple__labels are being saved with every element.
+ * Stop #multiple__label and #multiple__labels from being saved with every element.
  */
 function webform_update_8116() {
   _webform_update_elements_clear_properties([
@@ -2140,50 +2163,43 @@ function webform_update_8116() {
 }
 
 /**
- * Issue #2957192: Add postal_code to test data
+ * Issue #2957192: Add postal_code to test data.
  */
 function webform_update_8117() {
   $config = \Drupal::configFactory()->getEditable('webform.settings');
   $names = Yaml::decode($config->get('test.names'));
   $names += [
-    'postal_code' => ['11111', '12345', '12345-6789']
+    'postal_code' => ['11111', '12345', '12345-6789'],
   ];
-  $config->set('test.names', WebformYaml::tidy(Yaml::encode($names)));
+  $config->set('test.names', WebformYaml::encode($names));
   $config->save();
 }
 
+/******************************************************************************/
+// Webform-8.x-5.0-rc9 - April 7, 2018.
+/******************************************************************************/
+
 /**
  * Issue #2957074: Invalid Tokens in Email Handler.
  */
 function webform_update_8118() {
-  $config_factory = \Drupal::configFactory();
-
-  // Update 'webform.settings' configuration.
-  $settings_config = \Drupal::configFactory()->getEditable('webform.settings');
-  $yaml = Yaml::encode($settings_config->getRawData());
-  $yaml = str_replace('[webform-submission:', '[webform_submission:', $yaml);
-  $settings_config->setData(Yaml::decode($yaml));
-  $settings_config->save();
-
-  // Update 'webform.webform.*' configuration.
-  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
-    $webform_config = $config_factory->getEditable($webform_config_name);
-    $yaml = Yaml::encode($webform_config->getRawData());
-    $yaml = str_replace('[webform-submission:', '[webform_submission:', $yaml);
-    $webform_config->setData(Yaml::decode($yaml));
-    $webform_config->save();
-  }
+  _webform_update_string_replace('[webform-submission:', '[webform_submission:');
 }
 
-
 /**
  * Issue #2957002: Same webform multiple times on the same page.
  */
 function webform_update_8119() {
+  // Issue #2863986: Allow updating modules with new service dependencies.
+  \Drupal::service('kernel')->rebuildContainer();
+
+  // Issue #2982273: Route "webform.config.libraries" does not exist.
+  \Drupal::service('router.builder')->rebuild();
+
   /** @var \Drupal\webform\WebformHelpManagerInterface $help_manager */
   $help_manager = \Drupal::service('webform.help_manager');
   $help_manager->addNotification('webform_update_8119',
-    t('<strong>ATTENTION DEVELOPERS!!!</strong> The webform submission form\'s  BASE_FORM_ID and FORM_ID have changed.') .
+    t("<strong>ATTENTION DEVELOPERS!!!</strong> The webform submission form's  BASE_FORM_ID and FORM_ID have changed.") .
     '<br/>' . t('Please make sure to update all webform related <code>hook_form_BASE_FORM_ID_alter()</code> and <code>hook_form_FORM_ID_alter()</code> hooks.') .
     ' ' . t('<a href=":href">Learn more</a>', [':href' => 'https://www.drupal.org/node/2959264']),
     'warning'
@@ -2194,19 +2210,871 @@ function webform_update_8119() {
  * Issue #2953929: Remote handler does not display messages when HTTP status is different than 200.
  */
 function webform_update_8120() {
-  _webform_update_webform_handler_configuration(RemotePostWebformHandler::class);
+  _webform_update_webform_handler_settings();
 }
 
 /**
  * Issue #2953929: Remote handler does not display messages when HTTP status is different than 200.
  */
 function webform_update_8121() {
-  _webform_update_webform_handler_configuration(RemotePostWebformHandler::class);
+  _webform_update_webform_handler_settings();
 }
 
+/******************************************************************************/
+// Webform-8.x-5.0-rc10 - April 9, 2018 (No update required).
+/******************************************************************************/
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc11 - April 20, 2018 (No update required).
+/******************************************************************************/
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc12 - April 25 2018.
+/******************************************************************************/
+
 /**
- * Issue #2952419: Attached files are deleted without usage checking
+ * Issue #2952419: Attached files are deleted without usage checking.
  */
 function webform_update_8122() {
   _webform_update_admin_settings();
 }
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc13 - June 4, 2018.
+/******************************************************************************/
+
+/**
+ * Issue #2962442: Remove [webform-authenticated-user] token and use [current-user] token with clear value option.
+ */
+function webform_update_8123() {
+  _webform_update_string_replace('[webform-authenticated-user:', '[current-user:');
+}
+
+/**
+ * Issue #2971207: Hidden Field updated values not being captured on Submit.
+ */
+function webform_update_8124() {
+  $config_factory = \Drupal::configFactory();
+  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
+    $webform_config = $config_factory->getEditable($webform_config_name);
+    $data = $webform_config->getRawData();
+    if (strpos($data['elements'], "'#type': hidden") !== FALSE) {
+      $elements = Yaml::decode($data['elements']);
+      _webform_update_8124($elements);
+      $data['elements'] = Yaml::encode($elements);
+
+      $webform_config->setData($data);
+      $webform_config->save();
+    }
+  }
+}
+
+/**
+ * Recursively convert hidden elements #value to #default_value.
+ *
+ * @param array $element
+ *   An element.
+ */
+function _webform_update_8124(array &$element) {
+  if (isset($element['#type']) && $element['#type'] === 'hidden') {
+    if (isset($element['#value']) && !isset($element['#default_value'])) {
+      $element['#default_value'] = $element['#value'];
+      unset($element['#value']);
+    }
+  }
+  foreach (Element::children($element) as $key) {
+    if (is_array($element[$key])) {
+      _webform_update_8124($element[$key]);
+    }
+  }
+}
+
+/**
+ * Issue #2966507: Start-to-finish documentation for showing Webforms in modals.
+ */
+function webform_update_8125() {
+  _webform_update_admin_settings();
+}
+
+/**
+ * Issue #2973377: Make the previously saved messages customizable.
+ */
+function webform_update_8126() {
+  _webform_update_admin_settings();
+  _webform_update_webform_settings();
+}
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc14 - June 6, 2018.
+/******************************************************************************/
+
+/**
+ * Issue #2974597: Enable default publishing status of new webforms.
+ */
+function webform_update_8127() {
+  _webform_update_admin_settings();
+}
+
+/**
+ * Issue #2932893: Filter out closed forms in webform field.
+ */
+function webform_update_8128() {
+  _webform_update_webform_settings();
+}
+
+/**
+ * Issue #2977378: Add 'exclude unselected checkboxes' from email notification and preview.
+ */
+function webform_update_8129() {
+  _webform_update_admin_settings();
+  _webform_update_webform_settings();
+  _webform_update_webform_handler_settings();
+}
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc15 - June 12, 2018.
+/******************************************************************************/
+
+/**
+ * Issue #2974153: Non admin duplicate form.
+ */
+function webform_update_8130() {
+  _webform_update_webform_settings();
+}
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc16 - June 29, 2018.
+/******************************************************************************/
+
+/**
+ * Issue #2980470: Convert email handler "default" settings to use "_default_" to prevent localization issues.
+ */
+function webform_update_8131() {
+  // Get all webform handlers that are instances of the email handler.
+  // This allows any custom handler that extends the EmailWebformHandler to be
+  // updated.
+  /** @var \Drupal\webform\Plugin\WebformHandlerManagerInterface $handler_manager */
+  $handler_manager = \Drupal::service('plugin.manager.webform.handler');
+  $definitions = $handler_manager->getDefinitions();
+  $email_handler_ids = [];
+  foreach ($definitions as $plugin_id => $definition) {
+    if ($handler_manager->createInstance($plugin_id) instanceof EmailWebformHandler) {
+      $email_handler_ids[$plugin_id] = $plugin_id;
+    }
+  }
+
+  $default_settings = [
+    'to_mail',
+    'from_mail',
+    'from_name',
+    'subject',
+    'body',
+  ];
+
+  $config_factory = \Drupal::configFactory();
+  // Update 'webform.webform.*' configuration.
+  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
+    $webform_config = $config_factory->getEditable($webform_config_name);
+
+    // Get data.
+    $data = $webform_config->getRawData();
+
+    // Change 'default' to '_default' is email handler settings.
+    $save_handler = FALSE;
+    foreach ($data['handlers'] as &$handler) {
+      if (!isset($email_handler_ids[$handler['id']])) {
+        continue;
+      }
+
+      foreach ($handler['settings'] as $settings_key => $setting_value) {
+        if ($setting_value === 'default' && in_array($settings_key, $default_settings)) {
+          $handler['settings'][$settings_key] = EmailWebformHandler::DEFAULT_VALUE;
+          $save_handler = TRUE;
+        }
+      }
+    }
+
+    if ($save_handler) {
+      $webform_config->setData($data)->save();
+    }
+  }
+}
+
+/**
+ * Issue #2980276: Webform assumes the /tmp directory is always the same, but if there are multiple servers, each may have its own /tmp directory.
+ */
+function webform_update_8132() {
+  _webform_update_admin_settings();
+}
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc17 - August 6, 2018.
+/******************************************************************************/
+
+/**
+ * Issue #2890861: Webform toggle element is not accessible.
+ */
+function webform_update_8133() {
+  $config_factory = \Drupal::configFactory();
+  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
+    $webform_config = $config_factory->getEditable($webform_config_name);
+    $data = $webform_config->getRawData();
+    $yaml = Yaml::encode($data);
+    if (strpos($yaml, 'webform_toggle')) {
+      return;
+    }
+  }
+
+  $admin_config = \Drupal::configFactory()->getEditable('webform.settings');
+  $settings = $admin_config->getRawData();
+  $settings['libraries']['excluded_libraries'][] = 'jquery.toggles';
+  $admin_config->setData($settings)->save();
+}
+
+/**
+ * Issue #2984348: Link to relevant wizard page on preview.
+ */
+function webform_update_8134() {
+  _webform_update_admin_settings();
+}
+
+/**
+ * Issue #2984868: Allow to specify the text format for emails.
+ */
+function webform_update_8135() {
+  $admin_config = \Drupal::configFactory()->getEditable('webform.settings');
+  $settings = $admin_config->getRawData();
+  $settings['html_editor']['element_format'] = $settings['html_editor']['format'];
+  $settings['html_editor']['mail_format'] = $settings['html_editor']['format'];
+  unset($settings['html_editor']['format']);
+  $admin_config->setData($settings)->save();
+}
+
+/**
+ * Issue #2987174: Replace and improve word/character counter.
+ */
+function webform_update_8136() {
+  $config_factory = \Drupal::configFactory();
+  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
+    $webform_config = $config_factory->getEditable($webform_config_name);
+
+    $data = $webform_config->getRawData();
+    if (strpos($data['elements'], "'#counter_message'") === FALSE) {
+      continue;
+    }
+
+    $elements = Yaml::decode($data['elements']);
+    _webform_update_8136($elements);
+
+    $data['elements'] = Yaml::encode($elements);
+    $webform_config->setData($data);
+    $webform_config->save();
+  }
+}
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc18 - August 7, 2018 (No update required).
+/******************************************************************************/
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc19 - September 6, 2018.
+/******************************************************************************/
+
+/**
+ * Update #counter_* attributes.
+ *
+ * Replace 'X' in counter message with '%d' and
+ * move '#counter_message' to '#counter_maximum_message'.
+ *
+ * @param array $element
+ *   A form element.
+ */
+function _webform_update_8136(array &$element) {
+  if (isset($element['#counter_message'])) {
+    if (strpos($element['#counter_message'], 'X') === FALSE) {
+      $element['#counter_message'] = '%d ' . $element['#counter_message'];
+    }
+    else {
+      $element['#counter_message'] = str_replace('X', '%d', $element['#counter_message']);
+    }
+    $element['#counter_maximum_message'] = $element['#counter_message'];
+    unset($element['#counter_message']);
+  }
+
+  foreach (Element::children($element) as $key) {
+    if (is_array($element[$key])) {
+      _webform_update_8136($element[$key]);
+    }
+  }
+}
+
+/**
+ * Issue #2983137: WYSIWYG editor not saving inline images for advanced html/text editor.
+ */
+function webform_update_8137() {
+  _webform_update_admin_settings();
+}
+
+/**
+ * Issue #2947303: Location element's geocomplete library is not supported.
+ */
+function webform_update_8138() {
+  _webform_update_admin_settings();
+
+  // Track if webform location geocomplete is being used.
+  $has_geocomplete_element = FALSE;
+
+  // Convert 'webform_location' to 'webform_location_geocomplete'.
+  $config_factory = \Drupal::configFactory();
+  foreach ($config_factory->listAll('webform.webform.') as $config_name) {
+    $config = $config_factory->getEditable($config_name);
+    $elements = $config->get('elements');
+    if (strpos($elements, "'#type': webform_location") !== FALSE) {
+      $elements = str_replace("'#type': webform_location", "'#type': webform_location_geocomplete", $elements);
+      $config->set('elements', $elements);
+      $config->save(TRUE);
+      $has_geocomplete_element = TRUE;
+    }
+  }
+
+  // Update exclude elements.
+  $admin_config = \Drupal::configFactory()->getEditable('webform.settings');
+  $excluded_elements = $admin_config->get('element.excluded_elements') ?: [];
+  if (isset($excluded_elements['webform_location'])) {
+    // Convert 'webform_location' to 'webform_location_geocomplete'.
+    unset($excluded_elements['webform_location']);
+    $excluded_elements['webform_location_geocomplete'] = 'webform_location_geocomplete';
+  }
+  elseif (!$has_geocomplete_element) {
+    // If location geocomplete element is not being used then excluded it.
+    $excluded_elements['webform_location_geocomplete'] = 'webform_location_geocomplete';
+  }
+  $admin_config->set('element.excluded_elements', $excluded_elements);
+  $admin_config->save();
+}
+
+/**
+ * Issue #2918669: Elements separated by conditional inside multistep wizard are both saved to submission.
+ */
+function webform_update_8139() {
+  $build = [
+    'list' => [
+      '#theme' => 'item_list',
+      '#title' => t('IMPORTANT! Elements, containers, and pages that are hidden using conditional logic will now have their submission data cleared when a webform is submitted.'),
+      '#items' => [
+        t('Please make sure to test any webform that contains conditionally hidden elements, containers, or pages.'),
+        t('Any element that is conditionally hidden will have its submission data cleared.'),
+        t("Existing submissions that have conditionally hidden elements will have the element's submission data cleared when the submission is updated."),
+      ],
+    ],
+    'link' => [
+      '#type' => 'link',
+      '#title' => t('Learn more'),
+      '#url' => Url::fromUri('https://www.drupal.org/node/2956589'),
+    ],
+  ];
+
+  /** @var \Drupal\webform\WebformHelpManagerInterface $help_manager */
+  $help_manager = \Drupal::service('webform.help_manager');
+  $help_manager->addNotification(__FUNCTION__, $build, 'warning');
+}
+
+/**
+ * Issue #2994411: Allow help to be hidden.
+ */
+function webform_update_8140() {
+  _webform_update_admin_settings();
+}
+
+/**
+ * Issue #2993327: Provide Default View to Override Submission Results Tab.
+ */
+function webform_update_8141() {
+  _webform_update_admin_settings();
+  _webform_update_webform_settings();
+
+  // Copied from: redirect_update_8103()
+  $message = NULL;
+  if (\Drupal::moduleHandler()->moduleExists('views') && !View::load('webform_submissions')) {
+    $config_path = drupal_get_path('module', 'webform') . '/config/optional/views.view.webform_submissions.yml';
+    $data = ['uuid' => (new Uuid())->generate()] + Yaml::decode(file_get_contents($config_path));
+    \Drupal::configFactory()->getEditable('views.view.webform_submissions')->setData($data)->save(TRUE);
+    $message = 'The new webform submissions view has been created.';
+  }
+  else {
+    $message = 'Not creating a webform submissions view since it already exists.';
+  }
+  return $message;
+}
+
+/**
+ * Issue #2995413: Allow email handler theme to be customized.
+ */
+function webform_update_8142() {
+  _webform_update_webform_handler_settings();
+}
+
+/**
+ * Issue #2943879: How to display alternate text when the user is not allowed to create a Webform?
+ */
+function webform_update_8143() {
+  $admin_config = \Drupal::configFactory()->getEditable('webform.settings');
+  // Convert form login message to access denied message.
+  if ($admin_config->get('settings.default_form_login_message') !== NULL) {
+    $admin_config->set('settings.default_form_access_denied_message', $admin_config->get('settings.default_form_login_message'));
+    $admin_config->clear('settings.default_form_login_message');
+  }
+  $admin_config->save();
+  _webform_update_admin_settings();
+
+  $config_factory = \Drupal::configFactory();
+  foreach ($config_factory->listAll('webform.webform.') as $config_name) {
+    $config = $config_factory->getEditable($config_name);
+    // Convert form login to access denied.
+    if ($config->get('settings.form_login') !== NULL) {
+      $config->set('settings.form_access_denied', $config->get('settings.form_login') ? WebformInterface::ACCESS_DENIED_LOGIN : WebformInterface::ACCESS_DENIED_DEFAULT);
+      $config->clear('settings.form_login');
+    }
+    // Convert form login message to access denied message.
+    if ($config->get('settings.form_login_message') !== NULL) {
+      $config->set('settings.form_access_denied_message', $config->get('settings.form_login_message'));
+      $config->clear('settings.form_login_message');
+    }
+
+    // Convert submission login to access denied.
+    if ($config->get('settings.submission_login') !== NULL) {
+      $config->set('settings.submission_access_denied', $config->get('settings.submission_login') ? WebformInterface::ACCESS_DENIED_LOGIN : WebformInterface::ACCESS_DENIED_DEFAULT);
+      $config->clear('settings.submission_login');
+    }
+    // Convert submission login message to access denied message.
+    if ($config->get('settings.submission_login_message') !== NULL) {
+      $config->set('settings.submission_access_denied_message', $config->get('settings.submission_login_message'));
+      $config->clear('settings.submission_login_message');
+    }
+
+    $config->save();
+  }
+  _webform_update_webform_settings();
+}
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc20 - September 7, 2018 (No update required).
+/******************************************************************************/
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc21 - September 8, 2018 (No update required).
+/******************************************************************************/
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc22.
+/******************************************************************************/
+
+/**
+ * Issue #2998239: Swift Mailer no longer working after custom email handler theme option was added.
+ */
+function webform_update_8144() {
+  $config_factory = \Drupal::configFactory();
+  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
+    $webform_config = $config_factory->getEditable($webform_config_name);
+
+    $data = $webform_config->getRawData();
+    $has_email_handler = FALSE;
+    foreach ($data['handlers'] as &$handler) {
+      if (in_array($handler['id'], ['email', 'scheduled_email'])) {
+        // Change 'theme' setting to 'theme_name' to prevent conflicts with
+        // Swift Mailer.
+        if (isset($handler['settings']['theme'])) {
+          $handler['settings']['theme_name'] = $handler['settings']['theme'];
+          unset($handler['settings']['theme']);
+          $has_email_handler = TRUE;
+        }
+      }
+    }
+    if ($has_email_handler) {
+      $webform_config->setData($data);
+      $webform_config->save();
+    }
+  }
+}
+
+/**
+ * Issue #3002183: Upload Total Limit instead of Upload File limit.
+ */
+function webform_update_8145() {
+  _webform_update_admin_settings();
+  _webform_update_webform_settings();
+}
+
+/**
+ * Issue #3000542: Remove webform_devel.module logger debugging.
+ */
+function webform_update_8146() {
+  if ($config = \Drupal::configFactory()->getEditable('webform_devel.settings')) {
+    $config->delete();
+  }
+}
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc23 - October 20, 2018.
+/******************************************************************************/
+
+/**
+ * Issue #3006468: Hide empty fields on on submission page.
+ */
+function webform_update_8147() {
+  _webform_update_webform_settings();
+}
+
+/**
+ * Issue #3007215: Option to retain webform title when source entity is provided.
+ */
+function webform_update_8148() {
+  _webform_update_webform_settings();
+}
+
+/**
+ * Issue #3007247: Custom composite fields appear as block elements.
+ */
+function webform_update_8149() {
+  $config_factory = \Drupal::configFactory();
+  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
+    $webform_config = $config_factory->getEditable($webform_config_name);
+    $elements = $webform_config->get('elements');
+    if (strpos($elements, "'add_more'") !== FALSE) {
+      $elements = str_replace("'#add_more'", "'#add_more_items'", $elements);
+      $elements = str_replace("'#multiple__add_more'", "'#multiple__add_more_items'", $elements);
+      $webform_config->set('elements', $elements);
+      $webform_config->save(TRUE);
+    }
+  }
+}
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc24 - October 22, 2018.
+/******************************************************************************/
+
+/**
+ * Issue #2980032: SUBMISSION BEHAVIORS: Allow edit the previous submission.
+ */
+function webform_update_8150() {
+  _webform_update_webform_settings();
+}
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc25 - November 5, 2018 (No update required).
+/******************************************************************************/
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc26 - November 5, 2018 (No update required).
+/******************************************************************************/
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc27 - November 28, 2018.
+/******************************************************************************/
+
+/**
+ * Issue #3013767: Computed twig element is not working on multi-step form.
+ */
+function webform_update_8151() {
+  $config_factory = \Drupal::configFactory();
+  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
+    $webform_config = $config_factory->getEditable($webform_config_name);
+    $elements = $webform_config->get('elements');
+
+    // Try to decode elements.
+    try {
+      $elements = Yaml::decode($elements);
+    }
+    catch (\Exception $exception) {
+      continue;
+    }
+
+    // Make sure elements is an array.
+    if (!is_array($elements)) {
+      continue;
+    }
+
+    $has_computed_element = FALSE;
+    $flattened_elements =& WebformFormHelper::flattenElements($elements);
+    foreach ($flattened_elements as &$element) {
+      // Convert #value property to #template property.
+      // @see \Drupal\webform\Entity\Webform::initElementsRecursive
+      if (isset($element['#type']) && strpos($element['#type'], 'webform_computed_') === 0) {
+        $has_computed_element = TRUE;
+        if (isset($element['#value']) && !isset($element['#template'])) {
+          $element['#template'] = $element['#value'];
+          unset($element['#value']);
+        }
+      }
+    }
+    if ($has_computed_element) {
+      $webform_config->set('elements', Yaml::encode($elements));
+      $webform_config->save(TRUE);
+    }
+  }
+}
+
+/**
+ * Issue #3014933: Webform paths not being removed when a webform is deleted.
+ */
+function webform_update_8153() {
+  // Load all webforms to improve performance.
+  $webform = Webform::loadMultiple();
+
+  $database = \Drupal::database();
+  $select = $database->select('url_alias', 'u');
+  $select->fields('u', ['pid', 'source', 'alias', 'langcode']);
+  $select->condition('source', '/webform/%', 'LIKE');
+  $result = $select->execute();
+  while ($record = $result->fetchAssoc()) {
+    if (preg_match('#^/webform/([^/]+)/(?:drafts|submissions)$#', $record['source'], $match)) {
+      // Check if the webform still exists.
+      $webform_id = $match[1];
+      if (!isset($webform[$webform_id])) {
+        // Delete the broken URL alias.
+        $database->delete('url_alias')
+          ->condition('pid', $record['pid'])
+          ->execute();
+      }
+    }
+  }
+}
+
+/**
+ * Issue #3015990: Option not to store the IP address for logged-in users.
+ */
+function webform_update_8154() {
+  _webform_update_webform_settings();
+}
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc28 - December 7, 2018
+/******************************************************************************/
+
+/**
+ * Issue #3017679: 2 different validation range modes for date/time field.
+ */
+function webform_update_8155() {
+  $config_factory = \Drupal::configFactory();
+  foreach ($config_factory->listAll('webform.webform.') as $webform_config_name) {
+    $webform_config = $config_factory->getEditable($webform_config_name);
+    $elements = $webform_config->get('elements');
+
+    // Try to decode elements.
+    try {
+      $elements = Yaml::decode($elements);
+    }
+    catch (\Exception $exception) {
+      continue;
+    }
+
+    // Make sure elements is an array.
+    if (!is_array($elements)) {
+      continue;
+    }
+
+    $has_date_element = FALSE;
+    $flattened_elements =& WebformFormHelper::flattenElements($elements);
+    foreach ($flattened_elements as &$element) {
+      // Convert #min/max property to #date_date_(min|max) property.
+      // @see \Drupal\webform\Entity\Webform::initElementsRecursive
+      if (isset($element['#type']) && in_array($element['#type'], ['date', 'datetime', 'datelist'])) {
+        $has_date_element = TRUE;
+        if (isset($element['#min']) && !isset($element['#date_date_min'])) {
+          $element['#date_date_min'] = $element['#min'];
+          unset($element['#min']);
+        }
+        if (isset($element['#max']) && !isset($element['#date_date_max'])) {
+          $element['#date_date_max'] = $element['#max'];
+          unset($element['#max']);
+        }
+      }
+    }
+    if ($has_date_element) {
+      $webform_config->set('elements', Yaml::encode($elements));
+      $webform_config->save(TRUE);
+    }
+  }
+}
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc29 - December 7, 2018. (No update required).
+/******************************************************************************/
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc30 - December 16, 2018.
+/******************************************************************************/
+
+/**
+ * Issue #3015180: Add 'webform_submission_log' submodule.
+ */
+function webform_update_8156() {
+  $enable = \Drupal::config('webform.settings')->get('settings.default_submission_log');
+  if (!$enable) {
+    $query = \Drupal::entityQuery('webform')
+      ->condition('settings.submission_log', TRUE)
+      ->count();
+    $enable = $query->execute() > 0;
+  }
+
+  if ($enable) {
+    try {
+      \Drupal::service('module_installer')->install(['webform_submission_log']);
+    }
+    catch (\Drupal\Core\Database\SchemaObjectExistsException $exception) {
+      // This is actually expected. The table {webform_submission_log} would exist
+      // from webform submission entity schema.
+    }
+
+    // Because MySQL does not allow default value on blob/text column types and
+    // we want to add a not null blob column to a table that is likely to have
+    // existing rows, we are doing it in such a 3-step fashion.
+    \Drupal::database()->schema()->addField('webform_submission_log', 'variables', [
+      'type' => 'blob',
+      'size' => 'big',
+      'description' => 'Serialized array of variables that match the message string and that is passed into the t() function.',
+    ]);
+
+    \Drupal::database()->update('webform_submission_log')->fields([
+      'variables' => serialize([]),
+    ])->execute();
+
+    \Drupal::database()->schema()->changeField('webform_submission_log', 'variables', 'variables', [
+      'type' => 'blob',
+      'not null' => TRUE,
+      'size' => 'big',
+      'description' => 'Serialized array of variables that match the message string and that is passed into the t() function.',
+    ]);
+  }
+}
+
+/******************************************************************************/
+// Webform-8.x-5.0-rc31 - December 31, 2018. (No update required).
+/******************************************************************************/
+
+/******************************************************************************/
+// Webform-8.x-5.0 - December 24, 2018. (No update required).
+/******************************************************************************/
+
+/******************************************************************************/
+// Webform-8.x-5.1 - January 2, 2019.
+/******************************************************************************/
+
+/**
+ * Issue #3022398: Possible modification to update hook and/or documentation.
+ */
+function webform_update_8157() {
+  _webform_update_webform_submission_storage_schema();
+}
+
+/******************************************************************************/
+// Webform-8.x-5.2 - TDB.
+/******************************************************************************/
+
+/**
+ * Issue #3023863: Typo in State/Province codes options.
+ */
+function webform_update_8158() {
+  /** \Drupal\webform\WebformOptionsInterface */
+  $webform_options = WebformOptions::load('state_province_codes');
+  if (!$webform_options) {
+    return;
+  }
+  $options = $webform_options->getOptions();
+  if (isset($options['AE']) && $options['AE'] === 'Armed Forces (Canada, Europe, Africa, or Middle East') {
+    $options['AE'] = 'Armed Forces (Canada, Europe, Africa, or Middle East)';
+    $webform_options->setOptions($options)->save();
+  }
+}
+
+/**
+ * Issue #3023223: Remove unused fields settings.
+ */
+function webform_update_8159() {
+  /** @var \Drupal\field\FieldStorageConfigInterface[] $field_storage_configs */
+  $field_storage_configs = FieldStorageConfig::loadMultiple();
+  foreach ($field_storage_configs as $field_storage_config) {
+    if ($field_storage_config->getType() !== 'webform') {
+      continue;
+    }
+
+    list($entity_type, $field_name) = explode('.', $field_storage_config->getConfigTarget());
+    $bundles = $field_storage_config->getBundles();
+    foreach ($bundles as $bundle) {
+      $config = \Drupal::configFactory()
+        ->getEditable("field.field.$entity_type.$bundle.$field_name");
+      $data = $config->getRawData();
+      if (isset($data['settings']['default_data'])) {
+        unset(
+          $data['settings']['default_data'],
+          $data['settings']['status'],
+          $data['settings']['open'],
+          $data['settings']['close']
+        );
+        $config->setData($data)->save();
+      }
+    }
+  }
+}
+
+/**
+ * Issue #3019987: Fatal error: Allowed memory size after updating to rc29.
+ */
+function webform_update_8160() {
+  $config = \Drupal::configFactory()
+    ->getEditable('webform.webform.example_computed_elements');
+  if (!$config) {
+    return;
+  }
+
+  // Remove nested calculation elements.
+  $elements = WebformYaml::decode($config->get('elements'));
+  if (isset($elements['calculation']) && isset($elements['calculation']['calculation'])) {
+    $path = drupal_get_path('module', 'webform') . '/modules/webform_examples/config/install/webform.webform.example_computed_elements.yml';
+    $data = Yaml::decode(file_get_contents($path));
+    $config->set('elements', $data['elements'])->save();
+  }
+}
+
+/**
+ * Issue #3026068: Ensure the webform submission has the langcode entity key set.
+ */
+function webform_update_8161() {
+  // Installations that were updated from 8.x-5.0-beta10 or older might have a
+  // unclean langcode definition, update it. First make sure that the langcode
+  // entity key is set, if not, set it and update the field definition.
+  $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
+  $entity_type = $entity_definition_update_manager->getEntityType('webform_submission');
+  if (!$entity_type->getKey('langcode')) {
+    $keys = $entity_type->getKeys();
+    $keys['langcode'] = 'langcode';
+    $entity_type->set('entity_keys', $keys);
+    $entity_definition_update_manager->updateEntityType($entity_type);
+
+    $langcode_field = BaseFieldDefinition::create('language')
+      ->setName('langcode')
+      ->setTargetEntityTypeId('webform_submission')
+      ->setLabel(t('Language'))
+      ->setDescription(t('The submission language code.'));
+    \Drupal::entityDefinitionUpdateManager()->updateFieldStorageDefinition($langcode_field);
+  }
+}
+
+/**
+ * Issue #3035054: Captcha challenge still visible after 'close time'.
+ */
+function webform_update_8162() {
+  \Drupal::configFactory()
+    ->getEditable('webform.settings')
+    ->set('third_party_settings.captcha', [
+      'replace_administration_mode' => TRUE,
+    ])
+    ->save();
+}
+
+/**
+ * Issue #2902977: Provide tool for importing submissions from a CSV document.
+ */
+function webform_update_8163() {
+  _webform_update_admin_settings();
+}
diff --git a/web/modules/webform/includes/webform.libraries.inc b/web/modules/webform/includes/webform.libraries.inc
index e9e2b6e7d89283a8ece756d5271dac3414599ed1..60b034bec646a4328a0b3aea5ced7b97755826ae 100644
--- a/web/modules/webform/includes/webform.libraries.inc
+++ b/web/modules/webform/includes/webform.libraries.inc
@@ -9,15 +9,41 @@
  * Implements hook_library_info_alter().
  */
 function webform_library_info_alter(&$libraries, $extension) {
-  $webform_libraries_modules = \Drupal::moduleHandler()->getImplementations('webform_libraries_info');
-  $webform_libraries_modules[] = 'webform';
-
   // Only alter modules that declare webform libraries.
   // @see hook_webform_libraries_info()
+  $webform_libraries_modules = \Drupal::moduleHandler()->getImplementations('webform_libraries_info');
+  $webform_libraries_modules[] = 'webform';
   if (!in_array($extension, $webform_libraries_modules)) {
     return;
   }
 
+  // If chosen_lib.module is installed, then update the dependency.
+  if (\Drupal::moduleHandler()->moduleExists('chosen_lib')) {
+    if (isset($libraries['webform.element.chosen'])) {
+      $dependencies =& $libraries['webform.element.chosen']['dependencies'];
+      foreach ($dependencies as $index => $dependency) {
+        if ($dependency === 'webform/libraries.jquery.chosen') {
+          $dependencies[$index] = 'chosen_lib/chosen';
+          $dependencies[] = 'chosen_lib/chosen.css';
+          break;
+        }
+      }
+    }
+  }
+
+  // If select2.module is installed, then update the dependency.
+  if (\Drupal::moduleHandler()->moduleExists('select2')) {
+    if (isset($libraries['webform.element.select2'])) {
+      $dependencies =& $libraries['webform.element.select2']['dependencies'];
+      foreach ($dependencies as $index => $dependency) {
+        if ($dependency === 'webform/libraries.jquery.select2') {
+          $dependencies[$index] = 'select2/select2';
+          break;
+        }
+      }
+    }
+  }
+
   /** @var \Drupal\webform\WebformLibrariesManagerInterface $libraries_manager */
   $libraries_manager = \Drupal::service('webform.libraries_manager');
 
@@ -30,6 +56,11 @@ function webform_library_info_alter(&$libraries, $extension) {
       continue;
     }
 
+    // Skip libraries installed by other modules.
+    if (isset($library['module'])) {
+      continue;
+    }
+
     if (!empty($library['dependencies'])) {
       // Remove excluded libraries from dependencies.
       foreach ($library['dependencies'] as $dependency_index => $dependency_name) {
diff --git a/web/modules/webform/includes/webform.options.inc b/web/modules/webform/includes/webform.options.inc
index dd327eac86a83510bc66d4f59e53436bbeb1b43a..f5b1f1da86a94b586434a6db55628e10946e4ae3 100644
--- a/web/modules/webform/includes/webform.options.inc
+++ b/web/modules/webform/includes/webform.options.inc
@@ -73,3 +73,16 @@ function webform_webform_options_languages_alter(array &$options, array $element
     }
   }
 }
+
+/**
+ * Implements hook_webform_options_WEBFORM_OPTIONS_ID_alter().
+ */
+function webform_webform_options_translations_alter(array &$options, array $element = []) {
+  if (empty($options)) {
+    $languages = \Drupal::languageManager()->getLanguages();
+    $options = [];
+    foreach ($languages as $language) {
+      $options[$language->getId()] = $language->getName();
+    }
+  }
+}
diff --git a/web/modules/webform/includes/webform.theme.inc b/web/modules/webform/includes/webform.theme.inc
index 0d84821cfd0b4f5e4bad379bc1efce0a8201aa22..8a65c7f5d6490147073adb24a77b1e99d982a872 100644
--- a/web/modules/webform/includes/webform.theme.inc
+++ b/web/modules/webform/includes/webform.theme.inc
@@ -2,12 +2,15 @@
 
 /**
  * @file
- * Theme hooks, preprocessor, and suggesttions.
+ * Theme hooks, preprocessor, and suggestions.
  */
 
 use Drupal\file\Entity\File;
 use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Template\Attribute;
+use Drupal\webform\Utility\WebformAccessibilityHelper;
+use Drupal\webform\Utility\WebformElementHelper;
 
 /******************************************************************************/
 // Theme hooks.
@@ -67,6 +70,10 @@ function webform_theme() {
       'render element' => 'elements',
     ],
 
+    'webform_submission_form' => [
+      'render element' => 'form',
+    ],
+
     'webform_submission_navigation' => [
       'variables' => ['webform_submission' => NULL],
     ],
@@ -89,7 +96,7 @@ function webform_theme() {
     ],
 
     'webform_element_help' => [
-      'variables' => ['help' => NULL],
+      'variables' => ['help' => NULL, 'help_title' => '', 'attributes' => []],
     ],
 
     'webform_element_more' => [
@@ -112,6 +119,9 @@ function webform_theme() {
       'variables' => ['element' => [], 'value' => NULL, 'webform_submission' => NULL, 'options' => [], 'file' => NULL],
     ],
 
+    'webform_email_html' => [
+      'variables' => ['subject' => '', 'body' => '', 'webform_submission' => NULL, 'handler' => NULL],
+    ],
     'webform_email_message_html' => [
       'variables' => ['message' => '', 'webform_submission' => NULL, 'handler' => NULL],
     ],
@@ -119,6 +129,9 @@ function webform_theme() {
       'variables' => ['message' => '', 'webform_submission' => NULL, 'handler' => NULL],
     ],
 
+    'webform_html_editor_markup' => [
+      'variables' => ['markup' => NULL, 'allowed_tags' => []],
+    ],
     'webform_horizontal_rule' => [
       'render element' => 'element',
     ],
@@ -244,6 +257,10 @@ function webform_preprocess_menu_local_action(&$variables) {
  * @see \Drupal\webform\Plugin\WebformElement\OptionsBase
  */
 function webform_preprocess_checkboxes(&$variables) {
+  if (!WebformElementHelper::isWebformElement($variables['element'])) {
+    return;
+  }
+
   _webform_preprocess_options($variables);
 }
 
@@ -253,41 +270,100 @@ function webform_preprocess_checkboxes(&$variables) {
  * @see \Drupal\webform\Plugin\WebformElement\OptionsBase
  */
 function webform_preprocess_radios(&$variables) {
+  if (!WebformElementHelper::isWebformElement($variables['element'])) {
+    return;
+  }
+
   _webform_preprocess_options($variables);
 }
 
+/******************************************************************************/
+// Preprocess tables.
+/******************************************************************************/
+
 /**
- * Prepares variables for checkboxes and radios options templates.
- *
- * Below code must be called by template_preprocess_(radios|checkboxes) which
- * reset the element's 'attributes';
+ * Prepares variables for table templates.
  */
-function _webform_preprocess_options(&$variables) {
-  $element = $variables['element'];
+function webform_preprocess_table(&$variables) {
+  // Add links to 'Translate' webform tab.
+  if (\Drupal::routeMatch()->getRouteName() === 'entity.webform.config_translation_overview') {
+    /** @var \Drupal\webform\WebformInterface $webform */
+    $webform = \Drupal::routeMatch()->getParameter('webform');
+    foreach ($variables['rows'] as &$row) {
+      // Check first cell.
+      if (!isset($row['cells'][0]['content'])
+        || !is_array($row['cells'][0]['content'])
+        || !isset($row['cells'][0]['content']['#markup'])) {
+        continue;
+      }
 
-  $variables['attributes']['class'][] = Html::getClass('js-webform-' . $element['#type']);
+      // Check last cell edit link.
+      if (!isset($row['cells'][1]['content'])
+        || !is_array($row['cells'][1]['content'])
+        || !isset($row['cells'][1]['content']['#links'])
+        || !is_array($row['cells'][1]['content']['#links'])
+        || !isset($row['cells'][1]['content']['#links']['edit'])) {
+        continue;
+      }
 
-  if (!empty($element['#options_display'])) {
-    $variables['attributes']['class'][] = Html::getClass('webform-options-display-' . $element['#options_display']);
-    $variables['#attached']['library'][] = 'webform/webform.element.options';
+      // Get language from edit link.
+      $route_parameters = $row['cells'][1]['content']['#links']['edit']['url']->getRouteParameters();
+      $langcode = (isset($route_parameters['langcode'])) ? $route_parameters['langcode'] : NULL;
+      $language = \Drupal::languageManager()->getLanguage($langcode);
+
+      // Convert first to link.
+      $row['cells'][0]['content'] = [
+        '#type' => 'link',
+        '#url' => $webform->toUrl('canonical', ['language' => $language]),
+        '#title' => $row['cells'][0]['content'],
+      ];
+    }
   }
 }
 
+/******************************************************************************/
+// Preprocess containers.
+/******************************************************************************/
+
+/**
+ * Prepares variables for datetime form element templates.
+ */
+function webform_preprocess_datetime_form(&$variables) {
+  if (!WebformElementHelper::isWebformElement($variables['element'])) {
+    return;
+  }
+
+  // Add .container-inline to datetime form wrapper which is missing from the
+  // stable base theme.
+  // @see core/themes/classy/templates/form/datetime-form.html.twig
+  // @see core/themes/stable/templates/form/datetime-form.html.twig
+  $variables['attributes']['class'][] = 'container-inline';
+}
+
 /**
  * Prepares variables for details element templates.
  */
 function webform_preprocess_details(&$variables) {
-  // Move #description to #help for webform admin routes.
-  _webform_preprocess_description_help($variables);
+  if (!WebformElementHelper::isWebformElement($variables['element'])) {
+    return;
+  }
 
-  // Add (read) more to #description.
-  _webform_preprocess_details_description_more($variables);
+  // Setup description, help, and more.
+  _webform_preprocess_element($variables);
 
   $element = &$variables['element'];
 
-  // Restructure the details's title to include #help.
-  if (isset($variables['title'])) {
-    _webform_preprocess_help_title($variables['title'], $element);
+  // Hide details title.
+  if (isset($element['#title_display']) && $element['#title_display'] === 'invisible') {
+    $variables['title'] = WebformAccessibilityHelper::buildVisuallyHidden($variables['title']);
+  }
+
+  // Remove invalid 'required' and 'aria-required' attributes from details.
+  if (isset($element['#webform_key'])) {
+    unset(
+      $variables['attributes']['required'],
+      $variables['attributes']['aria-required']
+    );
   }
 }
 
@@ -295,227 +371,99 @@ function webform_preprocess_details(&$variables) {
  * Prepares variables for fieldset element templates.
  */
 function webform_preprocess_fieldset(&$variables) {
-  // Move #description to #help for webform admin routes.
-  _webform_preprocess_description_help($variables);
+  if (!WebformElementHelper::isWebformElement($variables['element'])) {
+    return;
+  }
 
-  // Add (read) more to #description.
-  _webform_preprocess_form_element_description_more($variables);
+  // Setup description, help, and more.
+  _webform_preprocess_element($variables, ['legend', 'title']);
 
   $element = &$variables['element'];
 
+  // If the description is displayed 'before' we need to move it to the
+  // fieldset's prefix.
+  // @see fieldset.html.twig
+  if (isset($element['#description_display']) && $element['#description_display'] === 'before' && !empty($variables['description']['content'])) {
+    if (isset($variables['prefix'])) {
+      if (is_array($variables['prefix'])) {
+        $variables['prefix'] = ['prefix' => $variables['prefix']];
+      }
+      else {
+        $variables['prefix'] = ['prefix' => ['#markup' => $variables['prefix']]];
+      }
+    }
+    else {
+      $variables['prefix'] = [];
+    }
+    $variables['prefix']['description'] = $variables['description']['content'];
+    unset($variables['description']['content']);
+  }
+
   // Apply inline title defined by radios, checkboxes, and buttons.
   // @see \Drupal\webform\Plugin\WebformElement\OptionsBase::prepare
   if (isset($element['#_title_display'])) {
     $variables['attributes']['class'][] = 'webform-fieldset--title-inline';
   }
 
-  // Restructure the fieldset's legend title to include #help.
-  if (isset($variables['legend']['title'])) {
-    _webform_preprocess_help_title($variables['legend']['title'], $element);
-  }
-
-  // Add .js-webform-form-composite class to be used #states API.
+  // Add .js-webform-form-type-* class to be used JavaScript and #states API.
+  // @see js/webform.element.location.geocomplete.js
   // @see js/webform.states.js
-  if (isset($element['#type']) && in_array($element['#type'], ['checkboxes', 'radios'])) {
-    $variables['attributes']['class'][] = 'js-webform-type-' . $element['#type'];
-    $variables['attributes']['class'][] = 'webform-type-' . $element['#type'];
+  if (isset($element['#type'])) {
+    $variables['attributes']['class'][] = 'js-webform-type-' . Html::getClass($element['#type']);
+    $variables['attributes']['class'][] = 'webform-type-' . Html::getClass($element['#type']);
   }
-}
-
-/**
- * Prepares variables for webform section element templates.
- */
-function webform_preprocess_webform_section(&$variables) {
-  // Move #description to #help for webform admin routes.
-  _webform_preprocess_description_help($variables);
 
-  $element = &$variables['element'];
-
-  // Restructure the section's title to include #help.
-  if (isset($variables['title'])) {
-    _webform_preprocess_help_title($variables['title'], $element);
-  }
-}
-
-/**
- * Prepares variables for datetime form wrapper templates.
- */
-function webform_preprocess_datetime_wrapper(&$variables) {
-  // Move #description to #help for webform admin routes.
-  _webform_preprocess_description_help($variables);
-
-  // Add (read) more to #description.
-  _webform_preprocess_form_element_description_more($variables);
-
-  $element = &$variables['element'];
-
-  // Restructure the title to include #help.
-  if (isset($variables['title'])) {
-    _webform_preprocess_help_title($variables['title'], $element);
+  // Remove invalid 'required' and 'aria-required' attributes from fieldset.
+  if (isset($element['#webform_key'])) {
+    unset(
+      $variables['attributes']['required'],
+      $variables['attributes']['aria-required']
+    );
   }
 }
 
-/**
- * Prepares variables for form label templates.
- */
-function webform_preprocess_form_element_label(&$variables) {
-  $element = &$variables['element'];
-
-  // Unset 'for' attribute.
-  // @see \Drupal\webform\Element\WebformOtherBase::processWebformOther
-  if (isset($element['#attributes']['for']) && $element['#attributes']['for'] === FALSE) {
-    unset($variables['attributes']['for']);
-  }
-
-  // Restructure the label's title to include #help.
-  if (!empty($element['#help'])) {
-    _webform_preprocess_help_title($variables['title'], $element);
-  }
-}
+/******************************************************************************/
+// Preprocess form element.
+/******************************************************************************/
 
 /**
  * Prepares variables for form element templates.
  */
 function webform_preprocess_form_element(&$variables) {
-  $element = &$variables['element'];
-
-  // Move #description to #help for webform admin routes.
-  _webform_preprocess_description_help($variables);
-
-  // Add (read) more to #description.
-  _webform_preprocess_form_element_description_more($variables);
-
-  // Add #help and #_title_display to label.
-  // Note: #_title_display is used to track inline titles.
-  // @see \Drupal\webform\Plugin\WebformElementBase::prepare
-  $variables['label'] += array_intersect_key($element, array_flip(['#help', '#_title_display']));
-}
-
-/**
- * Prepares #description and #help properties for form element templates.
- */
-function _webform_preprocess_description_help(&$variables) {
-  $element = &$variables['element'];
-  // Move #description to #help for webform admin routes.
-  if (\Drupal::config('webform.settings')->get('ui.description_help')
-    && \Drupal::service('webform.request')->isWebformAdminRoute()
-    && \Drupal::routeMatch()->getRouteName() != 'webform.contribute.settings'
-    && isset($element['#description'])
-    && !isset($element['#help'])
-    && !empty($element['#title'])
-    && !(isset($element['#title_display']) && in_array($element['#title_display'], ['attribute', 'invisible']))
-  ) {
-    $element['#help'] = $element['#description'];
-    unset($variables['description']);
-    if (is_array($element['#help'])) {
-      $element['#help'] = \Drupal::service('renderer')->render($element['#help']);
-    }
-  }
-}
-
-/**
- * Append #help to title variable.
- */
-function _webform_preprocess_help_title(&$variables, array &$element) {
-  if (empty($element['#help'])) {
+  if (!WebformElementHelper::isWebformElement($variables['element'])) {
     return;
   }
 
-  $variables = [
-    'title' => (is_array($variables)) ? $variables : ['#markup' => $variables],
-    'help' => [
-      '#type' => 'webform_help',
-    ] + array_intersect_key($element, array_flip(['#help'])),
-  ];
+  // Setup description, help, and more.
+  _webform_preprocess_element($variables);
 
-  // Get #title_display and move help before title for 'inline' titles.
-  if (isset($element['#_title_display'])) {
-    // #_title_display is set
-    // via \Drupal\webform\Plugin\WebformElementBase::prepare.
-    $title_display = $element['#_title_display'];
-  }
-  elseif (isset($element['#title_display'])) {
-    $title_display = $element['#title_display'];
-  }
-  else {
-    $title_display = NULL;
-  }
+  $element = &$variables['element'];
 
-  if ($title_display == 'inline') {
-    $variables['title']['#weight'] = 0;
-    $variables['help']['#weight'] = -1;
-  }
+  // Add #help, #help_attributes, and #_title_display to label.
+  // Note: #_title_display is used to track inline titles.
+  // @see \Drupal\webform\Plugin\WebformElementBase::prepare
+  $variables['label'] += array_intersect_key($element, array_flip(['#help', '#help_title', '#help_attributes', '#_title_display']));
 }
 
 /**
- * Prepares #more property for form element template.
- *
- * @see template_preprocess_form_element()
- * @see form-element.html.twig
- * @see template_preprocess_datetime_wrapper()
- * @see datetime-wrapper.html.twig
+ * Prepares variables for form label templates.
  */
-function _webform_preprocess_form_element_description_more(&$variables) {
+function webform_preprocess_form_element_label(&$variables) {
   $element = &$variables['element'];
-  if (empty($element['#more'])) {
-    return;
-  }
-
-  // Make sure $variables['description']['content'] is a render array.
-  if (!isset($variables['description'])) {
-    $variables['description_display'] = $element['#description_display'];
-    $description_attributes = [];
-    if (!empty($element['#id'])) {
-      $description_attributes['id'] = $element['#id'] . '--description';
-    }
-    $variables['description']['attributes'] = new Attribute($description_attributes);
-    $variables['description']['content'] = [];
-  }
 
-  // Move content to content.description and convert description to
-  // render array.
-  $variables['description']['content'] = [
-    'description' => is_array($variables['description']['content']) ? $variables['description']['content'] : ['#markup' => $variables['description']['content']],
-  ];
+  // Restructure the label's title to include #help.
+  _webform_preprocess_help_title($variables['title'], $element);
 
-  // If description display is invisible hie the description.
-  if (isset($variables['description_display']) && $variables['description_display'] == 'invisible') {
-    $variables['description']['content']['description']['#prefix'] = '<div class="visually-hidden">';
-    $variables['description']['content']['description']['#suffix'] = '</div>';
-    $variables['description_display'] = 'after';
+  // Remove label 'for' attribute.
+  if (!empty($element['#attributes']['webform-remove-for-attribute'])) {
+    unset($variables['attributes']['webform-remove-for-attribute']);
+    unset($variables['attributes']['for']);
   }
-
-  // Add more element.
-  $variables['description']['content']['more'] = [
-    '#type' => 'webform_more',
-    '#attributes' => (!empty($element['#id'])) ? ['id' => $element['#id'] . '--more'] : [],
-  ] + array_intersect_key($element, array_flip(['#more', '#more_title']));
 }
 
-/**
- * Prepares #more property for details template.
- *
- * @see template_preprocess_details()
- * @see details.html.twig
- */
-function _webform_preprocess_details_description_more(&$variables) {
-  $element = &$variables['element'];
-  if (empty($element['#more'])) {
-    return;
-  }
-
-  // Convert description to render array.
-  $variables['description'] = [
-    'description' => is_array($variables['description']) ? $variables['description'] : ['#markup' => $variables['description']],
-  ];
-
-  // Add more element.
-  $variables['description']['more'] = [
-    '#type' => 'webform_more',
-    '#attributes' => (!empty($element['#id'])) ? ['id' => $element['#id'] . '--more'] : [],
-  ] + array_intersect_key($element, array_flip(['#more', '#more_title']));
-
-}
+/******************************************************************************/
+// Preprocess file/image elements.
+/******************************************************************************/
 
 /**
  * Prepares variables for file managed file templates.
@@ -524,6 +472,10 @@ function _webform_preprocess_details_description_more(&$variables) {
  * @see template_preprocess_file_managed_file()
  */
 function webform_preprocess_file_managed_file(&$variables) {
+  if (!WebformElementHelper::isWebformElement($variables['element'])) {
+    return;
+  }
+
   $element = &$variables['element'];
   if (empty($element['#button'])) {
     return;
@@ -570,6 +522,16 @@ function webform_preprocess_file_managed_file(&$variables) {
   $element['#attached']['library'][] = 'webform/webform.element.file.button';
 }
 
+/**
+ * Prepares variables for file upload help text templates.
+ */
+function webform_preprocess_file_upload_help(&$variables) {
+  $upload_validators = $variables['upload_validators'];
+  if (isset($upload_validators['webform_file_limit']) && $upload_validators['webform_file_limit'][0]) {
+    $variables['descriptions'][] = t('@size limit per form.', ['@size' => format_size($upload_validators['webform_file_limit'][0])]);
+  }
+}
+
 /**
  * Prepares variables for file link templates.
  *
@@ -598,6 +560,226 @@ function webform_preprocess_image(&$variables) {
   }
 }
 
+/******************************************************************************/
+// Preprocess webform specific elements.
+/******************************************************************************/
+
+/**
+ * Prepares variables for webform section element templates.
+ */
+function webform_preprocess_webform_section(&$variables) {
+  // Setup description, help, and more.
+  _webform_preprocess_element($variables);
+}
+
+/******************************************************************************/
+// Preprocess helpers.
+/******************************************************************************/
+
+/**
+ * Prepares variables for checkboxes and radios options templates.
+ *
+ * Below code must be called by template_preprocess_(radios|checkboxes) which
+ * reset the element's 'attributes';
+ */
+function _webform_preprocess_options(array &$variables) {
+  $element =& $variables['element'];
+
+  $variables['attributes']['class'][] = Html::getClass('js-webform-' . $element['#type']);
+
+  if (!empty($element['#options_display'])) {
+    $variables['attributes']['class'][] = Html::getClass('webform-options-display-' . $element['#options_display']);
+    $variables['#attached']['library'][] = 'webform/webform.element.options';
+  }
+}
+
+/**
+ * Prepares webform element description, help, and more templates.
+ *
+ * @see template_preprocess_form_element()
+ * @see core/modules/system/templates/form-element.html.twig
+ * @see template_preprocess_details()
+ * @see /core/modules/system/templates/details.html.twig
+ * @see template_preprocess_fieldset()
+ * @see /core/modules/system/templates/fieldset.html.twig
+ * @see template_preprocess_webform_section()
+ * @see /webform/templates/webform-section.html.twig
+ */
+function _webform_preprocess_element(array &$variables, $title_parents = ['title']) {
+  $element =& $variables['element'];
+  $type = $element['#type'];
+
+  // Fix details 'description' property which does not have description.content.
+  // @see template_preprocess_details
+  // @see Issue #2896169: Details elements have incorrect aria-describedby attributes
+  if (!empty($element['#description'])) {
+    // Convert the current description to simple #markup.
+    if (is_array($element['#description'])) {
+      // Make a copy of the element's #description so that it can be rendered
+      // without affecting the element's #description.
+      $element_description = $element['#description'];
+      $description = ['#markup' => \Drupal::service('renderer')->render($element_description)];
+    }
+    else {
+      $description = ['#markup' => $element['#description']];
+    }
+
+    if ($type === 'details') {
+      $description_attributes = [];
+      if (!empty($element['#id'])) {
+        $description_attributes['id'] = $element['#id'] . '--description';
+      }
+      $variables['description'] = [];
+      $variables['description']['content'] = [
+        '#type' => 'container',
+        '#attributes' => new Attribute($description_attributes),
+      ] + $description;
+    }
+    else {
+      // Wrap description in a container.
+      $variables['description']['content'] = [
+        '#type' => 'container',
+        '#attributes' => $variables['description']['attributes'],
+      ] + $description;
+      $variables['description']['attributes'] = new Attribute();
+    }
+
+    $variables['description']['content']['#attributes']->addClass('webform-element-description');
+
+    // Handle invisible descriptions.
+    if (isset($element['#description_display']) && $element['#description_display'] === 'invisible') {
+      $variables['description']['content']['#attributes']->addClass('visually-hidden');
+      $variables['description_display'] = 'after';
+    }
+
+    // Nest description content so that we can a more link
+    // below the description.
+    $variables['description']['content'] = [
+      'description' => $variables['description']['content'],
+    ];
+  }
+  elseif (isset($variables['description']) && empty($variables['description'])) {
+    // Unset $variable['description'] which can be set to NULL or empty string.
+    // This allows $variable['description'] to be converted to render array.
+    // @see template_preprocess_details()
+    // @see template_preprocess_form_element()
+    unset($variables['description']);
+  }
+
+  $title =& NestedArray::getValue($variables, $title_parents);
+
+  // Move #description to #help for webform admin routes.
+  _webform_preprocess_description_help($variables);
+
+  // Add (read) more to #description.
+  _webform_preprocess_form_element_description_more($variables);
+
+  // Add help to title (aka label).
+  _webform_preprocess_help_title($title, $element);
+}
+
+/**
+ * Prepares #description and #help properties for form element templates.
+ */
+function _webform_preprocess_description_help(array &$variables) {
+  $element = &$variables['element'];
+  // Move #description to #help for webform admin routes.
+  if (\Drupal::config('webform.settings')->get('ui.description_help')
+    && \Drupal::service('webform.request')->isWebformAdminRoute()
+    && \Drupal::routeMatch()->getRouteName() != 'webform.contribute.settings'
+    && !isset($element['#help'])
+    && !empty($element['#title']) && (empty($element['#title_display']) || !in_array($element['#title_display'], ['attribute', 'invisible']))
+    && !empty($element['#description']) && (empty($element['#description_display']) || !in_array($element['#description_display'], ['invisible']))
+  ) {
+    // Render the description.
+    $description = (is_array($element['#description'])) ? \Drupal::service('renderer')->render($element['#description']) : $element['#description'];
+    // Replace breaks in admin tooltips with horizontal rules.
+    $description = str_replace('<br /><br />', '<hr />', $description);
+    $element['#help'] = ['#markup' => $description];
+
+    // We must still render the description as visually hidden because the input
+    // has an 'aria-describedby' attribute pointing to the description's id.
+    $variables['description_display'] = 'after';
+    $variables['description']['content']['description']['#attributes']->addClass('visually-hidden');
+
+    // Remove all links from the #description since it will be .visually-hidden
+    // and unreachable via tabbing.
+    if (isset($variables['description']['content']['description']['#markup'])) {
+      $variables['description']['content']['description']['#markup'] = strip_tags($variables['description']['content']['description']['#markup']);
+    }
+  }
+}
+
+/**
+ * Append #help to title variable.
+ */
+function _webform_preprocess_help_title(&$variables, array &$element) {
+  if (empty($variables) || empty($element['#help'])) {
+    return;
+  }
+
+  // Default #help_title to element's #title.
+  if (empty($element['#help_title']) && !empty($element['#title'])) {
+    $element['#help_title'] = $element['#title'];
+  }
+
+  $variables = [
+    'title' => (is_array($variables)) ? $variables : ['#markup' => $variables],
+    'help' => [
+      '#type' => 'webform_help',
+    ] + array_intersect_key($element, array_flip(['#help', '#help_title'])),
+  ];
+
+  // Add help attributes.
+  if (isset($element['#help_attributes'])) {
+    $variables['help']['#attributes'] = $element['#help_attributes'];
+  }
+
+  // Get #title_display and move help before title for 'inline' titles.
+  if (isset($element['#_title_display'])) {
+    // #_title_display is set via WebformElementBase::prepare.
+    // @see \Drupal\webform\Plugin\WebformElementBase::prepare.
+    $title_display = $element['#_title_display'];
+  }
+  elseif (isset($element['#title_display'])) {
+    $title_display = $element['#title_display'];
+  }
+  else {
+    $title_display = NULL;
+  }
+
+  if ($title_display === 'inline') {
+    $variables['title']['#weight'] = 0;
+    $variables['help']['#weight'] = -1;
+  }
+}
+
+/**
+ * Prepares #more property for form element template.
+ *
+ * @see template_preprocess_form_element()
+ * @see form-element.html.twig
+ * @see template_preprocess_datetime_wrapper()
+ * @see datetime-wrapper.html.twig
+ */
+function _webform_preprocess_form_element_description_more(array &$variables) {
+  $element = &$variables['element'];
+  if (empty($element['#more'])) {
+    return;
+  }
+
+  // Make sure description is displayed.
+  if (!isset($variables['description_display'])) {
+    $variables['description_display'] = 'after';
+  }
+
+  // Add more element.
+  $variables['description']['content']['more'] = [
+    '#type' => 'webform_more',
+    '#attributes' => (!empty($element['#id'])) ? ['id' => $element['#id'] . '--more'] : [],
+  ] + array_intersect_key($element, array_flip(['#more', '#more_title']));
+}
+
 /******************************************************************************/
 // Theme suggestions.
 /******************************************************************************/
@@ -619,6 +801,9 @@ function _webform_theme_suggestions(array $variables, $hook) {
   if ($hook == 'webform' && isset($variables['element']) && isset($variables['element']['#webform_id'])) {
     $suggestions[] = $hook . '__' . $variables['element']['#webform_id'];
   }
+  elseif ($hook == 'webform_submission_form' && isset($variables['form']) && isset($variables['form']['#webform_id'])) {
+    $suggestions[] = $hook . '__' . $variables['form']['#webform_id'];
+  }
   elseif (strpos($hook, 'webform_element_base_') === 0 || strpos($hook, 'webform_container_base_') === 0) {
     $element = $variables['element'];
 
@@ -634,7 +819,7 @@ function _webform_theme_suggestions(array $variables, $hook) {
     $webform = NULL;
     $webform_submission = NULL;
     $sanitized_view_mode = NULL;
-    if (isset($variables['elements']) || isset($variables['elements']['#webform_submission'])) {
+    if (isset($variables['elements']) && isset($variables['elements']['#webform_submission'])) {
       /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
       $webform_submission = $variables['elements']['#webform_submission'];
       $webform = $webform_submission->getWebform();
@@ -743,6 +928,13 @@ function webform_theme_suggestions_webform_submission(array $variables) {
   return _webform_theme_suggestions($variables, 'webform_submission');
 }
 
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function webform_theme_suggestions_webform_submission_form(array $variables) {
+  return _webform_theme_suggestions($variables, 'webform_submission_form');
+}
+
 /**
  * Implements hook_theme_suggestions_HOOK().
  */
@@ -778,6 +970,13 @@ function webform_theme_suggestions_webform_container_base_text(array $variables)
   return _webform_theme_suggestions($variables, 'webform_container_base_text');
 }
 
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function webform_theme_suggestions_webform_email_html(array $variables) {
+  return _webform_theme_suggestions($variables, 'webform_email_html');
+}
+
 /**
  * Implements hook_theme_suggestions_HOOK().
  */
diff --git a/web/modules/webform/includes/webform.theme.template.inc b/web/modules/webform/includes/webform.theme.template.inc
index 9f22e4d6c2d58854b2f1a1096730112bb0935984..b94eb8ed1d4716d095ed9223a7684292b4a23411 100644
--- a/web/modules/webform/includes/webform.theme.template.inc
+++ b/web/modules/webform/includes/webform.theme.template.inc
@@ -5,6 +5,7 @@
  * Preprocessors and helper functions to make theming easier.
  */
 
+use Drupal\Component\Utility\Html;
 use Drupal\Core\Link;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\Markup;
@@ -15,6 +16,7 @@
 use Drupal\webform\Element\WebformCodeMirror;
 use Drupal\webform\Element\WebformHtmlEditor;
 use Drupal\webform\WebformMessageManagerInterface;
+use Drupal\webform\WebformSubmissionInterface;
 use Drupal\webform\Utility\WebformDateHelper;
 use Drupal\webform\Utility\WebformDialogHelper;
 use Drupal\webform\Utility\WebformElementHelper;
@@ -169,7 +171,7 @@ function template_preprocess_webform_confirmation(array &$variables) {
   }
 
   // Set message.
-  $variables['message'] = $message_manager->build(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION);
+  $variables['message'] = $message_manager->build(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION_MESSAGE);
 
   // Set attributes.
   $variables['attributes'] = new Attribute($settings['confirmation_attributes']);
@@ -232,6 +234,11 @@ function template_preprocess_webform_submission_navigation(array &$variables) {
 
   /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
   $webform_submission = $variables['webform_submission'];
+  $webform = $webform_submission->getWebform();
+
+  // Webform id and title for context.
+  $variables['webform_id'] = $webform->id();
+  $variables['webform_title'] = $webform->label();
 
   // Get the route name, parameters, and source entity for the current page.
   // This ensures that the user stays within their current context as they are
@@ -276,9 +283,15 @@ function template_preprocess_webform_submission_navigation(array &$variables) {
  */
 function template_preprocess_webform_submission(array &$variables) {
   $variables['view_mode'] = $variables['elements']['#view_mode'];
-  $variables['webform_submission'] = $variables['elements']['#webform_submission'];
-  $variables['webform'] = $variables['elements']['#webform_submission']->getWebform();
 
+  $variables['navigation'] = $variables['elements']['navigation'];
+  $variables['information'] = $variables['elements']['information'];
+  $variables['submission'] = $variables['elements']['submission'];
+
+  $variables['webform_submission'] = $variables['elements']['#webform_submission'];
+  if ($variables['webform_submission'] instanceof WebformSubmissionInterface) {
+    $variables['webform'] = $variables['webform_submission']->getWebform();
+  }
 }
 
 /**
@@ -295,7 +308,6 @@ function template_preprocess_webform_submission_information(array &$variables) {
   $webform_submission = $variables['webform_submission'];
   $webform = $webform_submission->getWebform();
 
-  $variables['webform_id'] = $webform->id();
   $variables['serial'] = $webform_submission->serial();
   $variables['sid'] = $webform_submission->id();
   $variables['uuid'] = $webform_submission->uuid();
@@ -317,7 +329,7 @@ function template_preprocess_webform_submission_information(array &$variables) {
   $variables['language'] = isset($languages[$langcode]) ? $languages[$langcode]->getName() : $langcode;
 
   if ($source_url = $webform_submission->getSourceUrl()) {
-    $variables['uri'] = Link::fromTextAndUrl($source_url->setAbsolute(FALSE)->toString(), $source_url);;
+    $variables['uri'] = Link::fromTextAndUrl($source_url->setAbsolute(FALSE)->toString(), $source_url);
   }
 
   if ($webform->getSetting('token_update')) {
@@ -345,6 +357,22 @@ function template_preprocess_webform_submission_information(array &$variables) {
       $variables['submissions_view'] = TRUE;
     }
   }
+
+  if ($webform_submission->access('delete')) {
+    /** @var \Drupal\webform\WebformRequestInterface $request_handler */
+    $request_handler = \Drupal::service('webform.request');
+    $base_route_name = (strpos(\Drupal::routeMatch()->getRouteName(), 'webform.user.submission') !== FALSE) ? 'webform.user.submission.delete' : 'webform_submission.delete_form';
+    $url = $request_handler->getUrl($webform_submission, $source_entity, $base_route_name);
+
+    $variables['delete'] = [
+      '#type' => 'link',
+      '#title' => t('Delete submission'),
+      '#url' => $url,
+      '#attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW, ['button', 'button--danger']),
+    ];
+
+    WebformDialogHelper::attachLibraries($variables['delete']);
+  }
 }
 
 /**
@@ -390,19 +418,21 @@ function template_preprocess_webform_element_base_html(array &$variables) {
   if (empty($variables['options']['email'])) {
     $type = $element['#type'];
 
+    $attributes = (isset($element['#format_attributes'])) ? $element['#format_attributes'] : [];
+    $attributes += ['class' => []];
+    // Use wrapper attributes for the id instead of #id,
+    // this stops the <label> from having a 'for' attribute.
+    $attributes += [
+      'id' => $element['#webform_id'],
+    ];
+    $attributes['class'][] = 'webform-element';
+    $attributes['class'][] = 'webform-element-type-' . str_replace('_', '-', $type);
+
     $variables['item'] = [
       '#type' => 'item',
       '#title' => $variables['title'],
       '#name' => $element['#webform_key'],
-      // Use wrapper attributes for the id instead of #id,
-      // this stops the <label> from having a 'for' attribute.
-      '#wrapper_attributes' => [
-        'id' => $element['#webform_id'],
-        'class' => [
-          'webform-element',
-          'webform-element-type-' . str_replace('_', '-', $type),
-        ],
-      ],
+      '#wrapper_attributes' => $attributes,
     ];
     if (is_array($variables['value'])) {
       $variables['item']['value'] = $variables['value'];
@@ -473,8 +503,8 @@ function _template_progress_webform_set_title(array &$variables, $strip_tags = F
  *   - current_page: The current wizard page.
  */
 function template_preprocess_webform_progress(array &$variables) {
-  /** @var \Drupal\webform\WebformLibrariesManagerInterface $libaries_manager */
-  $libaries_manager = \Drupal::service('webform.libraries_manager');
+  /** @var \Drupal\webform\WebformLibrariesManagerInterface $libraries_manager */
+  $libraries_manager = \Drupal::service('webform.libraries_manager');
 
   /** @var \Drupal\webform\WebformInterface $webform */
   $webform = $variables['webform'];
@@ -491,7 +521,7 @@ function template_preprocess_webform_progress(array &$variables) {
 
   if ($webform->getSetting('wizard_progress_bar')) {
     $variables['bar'] = [
-      '#theme' => ($libaries_manager->isIncluded('progress-tracker')) ? 'webform_progress_tracker' : 'webform_progress_bar',
+      '#theme' => ($libraries_manager->isIncluded('progress-tracker')) ? 'webform_progress_tracker' : 'webform_progress_bar',
       '#webform' => $webform,
       '#current_page' => $current_page,
       '#operation' => $operation,
@@ -604,6 +634,23 @@ function template_preprocess_webform_message(array &$variables) {
   }
 }
 
+/**
+ * Prepares variables for Webform HTML Editor markup templates.
+ *
+ * Default template: webform-html-editor-markup.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - markup: HTML markup.
+ *   - allowed_tags: Allowed tags.
+ */
+function template_preprocess_webform_html_editor_markup(array &$variables) {
+  $variables['content'] = [
+    '#markup' => $variables['markup'],
+    '#allowed_tags' => $variables['allowed_tags'],
+  ];
+}
+
 /**
  * Prepares variables for Webform horizontal rule templates.
  *
@@ -789,19 +836,24 @@ function template_preprocess_webform_composite_telephone(array &$variables) {
  *   An associative array containing the following key:
  *   - element: The webform element.
  *   - help: The help content.
+ *   - attributes: The help attributes.
  */
 function template_preprocess_webform_element_help(array &$variables) {
-  $variables['help_icon'] = [
-    '#type' => 'link',
-    '#title' => '?',
-    '#url' => Url::fromRoute('<none>', [], ['fragment' => 'help']),
-    '#attributes' => [
-      'title' => $variables['help'],
-      // Add 'data-webform-help' attribute which support HTML markup.
-      'data-webform-help' => WebformHtmlEditor::stripTags($variables['help']),
-      'class' => ['webform-element-help'],
-    ],
-  ];
+  $attributes = (isset($variables['attributes'])) ? $variables['attributes'] : [];
+  $attributes['class'][] = 'webform-element-help';
+  $attributes['role'] = 'tooltip';
+  $attributes['tabindex'] = '0';
+
+  $content = (is_array($variables['help'])) ? \Drupal::service('renderer')->render($variables['help']) : $variables['help'];
+
+  $help = '';
+  if (!empty($variables['help_title'])) {
+    $help .= '<div class="webform-element-help--title">' . WebformHtmlEditor::stripTags($variables['help_title']) . '</div>';
+  }
+  $help .= '<div class="webform-element-help--content">' . WebformHtmlEditor::stripTags($content) . '</div>';
+  $attributes['data-webform-help'] = $help;
+
+  $variables['attributes'] = new Attribute($attributes);
 }
 
 /**
@@ -826,6 +878,16 @@ function template_preprocess_webform_element_more(array &$variables) {
   }
 
   $variables['attributes'] = new Attribute($variables['attributes']);
+
+  // Make sure there is a unique id.
+  if (empty($variables['id'])) {
+    $variables['id'] = Html::getUniqueId('webform-element-more');
+  }
+
+  // Make sure attributes id is set.
+  if (!isset($variables['attributes']['id'])) {
+    $variables['attributes']['id'] = $variables['id'];
+  }
 }
 
 /**
@@ -909,8 +971,11 @@ function template_preprocess_webform_element_image_file(array &$variables) {
     $uri = $file->getFileUri();
     $url = Url::fromUri(file_create_url($uri));
 
+    $extension = pathinfo($uri, PATHINFO_EXTENSION);
+    $is_image = in_array($extension, ['gif', 'png', 'jpg', 'jpeg']);
+
     // Build image.
-    if (\Drupal::moduleHandler()->moduleExists('image') && $style_name && ImageStyle::load($style_name)) {
+    if ($is_image && \Drupal::moduleHandler()->moduleExists('image') && $style_name && ImageStyle::load($style_name)) {
       $variables['image'] = [
         '#theme' => 'image_style',
         '#style_name' => $variables['style_name'],
diff --git a/web/modules/webform/includes/webform.translation.inc b/web/modules/webform/includes/webform.translation.inc
index 4adcebaaf54cbe53678cda842cc575a0789539db..03894924ac55313b74871d32f55dafd2a0357b48 100644
--- a/web/modules/webform/includes/webform.translation.inc
+++ b/web/modules/webform/includes/webform.translation.inc
@@ -3,6 +3,8 @@
 /**
  * @file
  * Webform module translation hooks.
+ *
+ * @see webform_preprocess_table()
  */
 
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
@@ -13,13 +15,12 @@
 use Drupal\Core\Serialization\Yaml;
 use Drupal\webform\Utility\WebformElementHelper;
 
-
 /**
  * Implements hook_form_FORM_ID_alter().
  */
 function webform_form_locale_translate_edit_form_alter(&$form, FormStateInterface $form_state) {
   // Don't allow YAML to be validated using locale string translation.
-  foreach (\Drupal\Core\Render\Element::children($form['strings']) as $key) {
+  foreach (Element::children($form['strings']) as $key) {
     $element =& $form['strings'][$key];
     if ($element['original']
       && !empty($element['original']['#plain_text'])
@@ -58,11 +59,11 @@ function webform_form_config_translation_add_form_alter(&$form, FormStateInterfa
 
       /** @var \Drupal\webform\WebformTranslationManagerInterface $translation_manager */
       $translation_manager = \Drupal::service('webform.translation_manager');
-      $translation_langcode = $form_state->get('config_translation_language')->getId();;
+      $translation_langcode = $form_state->get('config_translation_language')->getId();
       $source_elements = $translation_manager->getSourceElements($webform);
       $translation_elements = $translation_manager->getTranslationElements($webform, $translation_langcode);
-      $source_value = trim(Yaml::encode($source_elements));
-      $translation_value = trim(Yaml::encode($translation_elements));
+      $source_value = WebformYaml::encode($source_elements);
+      $translation_value = WebformYaml::encode($translation_elements);
 
       _webform_form_config_translate_add_form_alter_yaml_element($config_element['elements'], $source_value, $translation_value);
 
@@ -80,6 +81,10 @@ function webform_form_config_translation_add_form_alter(&$form, FormStateInterfa
  * Validate callback; Validates and cleanups webform elements.
  */
 function _webform_form_config_translate_add_form_validate(&$form, FormStateInterface $form_state) {
+  if ($form_state::hasAnyErrors()) {
+    return;
+  }
+
   $values = $form_state->getValues();
 
   $config_name = $form_state->get('webform_config_name');
@@ -172,26 +177,32 @@ function _webform_form_config_translate_add_form_alter_yaml_element(array &$elem
  */
 function webform_lingotek_config_entity_document_upload(array &$source_data, ConfigEntityInterface &$entity, &$url) {
   switch ($entity->getEntityTypeId()) {
+    case 'field_config':
+      // Convert webform default data YAML string to an associative array.
+      /** @var \Drupal\field\Entity\FieldConfig $entity */
+      if ($entity->getFieldStorageDefinition()->getType() === 'webform') {
+        foreach ($source_data as &$field_settings) {
+          foreach ($field_settings as $setting_name => $setting_value) {
+            if (preg_match('/\.default_data$/', $setting_name)) {
+              $field_settings[$setting_name] = Yaml::decode($field_settings[$setting_name]);
+            }
+          }
+        }
+        _webform_lingotek_encode_tokens($field_settings);
+      }
+      break;
+
     case 'webform';
       /** @var \Drupal\webform\WebformTranslationManagerInterface $translation_manager */
       $translation_manager = \Drupal::service('webform.translation_manager');
 
       // Replace elements with just the translatable properties
-      // (i.e. #title, #description, #options, etc...) so that Lingotek's
+      // (i.e. #title, #description, #options, etc…) so that Lingotek's
       // translation services can correctly translate each element.
       $translation_elements = $translation_manager->getTranslationElements($entity, $entity->language()->getId());
       $source_data['elements'] = $translation_elements;
 
-      // Encode all [tokens].
-      $yaml = Yaml::encode($source_data);
-      $yaml = preg_replace_callback(
-        '/\[([a-z][^]]+)\]/',
-        function ($matches) {
-          return '[***' . base64_encode($matches[1]) . '***]';
-        },
-        $yaml
-      );
-      $source_data = Yaml::decode($yaml);
+      _webform_lingotek_encode_tokens($source_data);
       break;
 
     case 'webform_options';
@@ -206,17 +217,23 @@ function ($matches) {
  */
 function webform_lingotek_config_entity_translation_presave(ConfigEntityInterface &$translation, $langcode, &$data) {
   switch ($translation->getEntityTypeId()) {
+    case 'field_config':
+      // Convert webform default data associative array back to YAML string.
+      /** @var \Drupal\field\Entity\FieldConfig $translation */
+      if ($translation->getFieldStorageDefinition()->getType() === 'webform') {
+        foreach ($data as &$field_settings) {
+          _webform_lingotek_encode_tokens($field_settings);
+          foreach ($field_settings as $setting_name => $setting_value) {
+            if (preg_match('/\.default_data$/', $setting_name)) {
+              $field_settings[$setting_name] = $field_settings[$setting_name] ? Yaml::encode($field_settings[$setting_name]) : '';
+            }
+          }
+        }
+      }
+      break;
+
     case 'webform';
-      // Decode all [tokens].
-      $yaml = Yaml::encode($data);
-      $yaml = preg_replace_callback(
-        '/\[\*\*\*([^]]+)\*\*\*\]/',
-        function ($matches) {
-          return '[' . base64_decode($matches[1]) . ']';
-        },
-        $yaml
-      );
-      $data = Yaml::decode($yaml);
+      _webform_lingotek_decode_tokens($data);
 
       /** @var \Drupal\webform\WebformInterface $translation */
       $translation->setElements($data['elements']);
@@ -233,17 +250,75 @@ function ($matches) {
 }
 
 /**
- * Implements hook_form_FORM_ID_alter().
+ * Implements hook_lingotek_config_object_document_upload().
  */
-function webform_form_lingotek_config_management_alter(&$form, FormStateInterface $form_state, $form_id) {
-  $entity_type = $form['filters']['wrapper']['bundle']['#default_value'];
-  if (!in_array($entity_type, ['webform', 'webform_options'])) {
+function webform_lingotek_config_object_document_upload(array &$data, $config_name) {
+  if ($config_name !== 'webform.settings') {
     return;
   }
 
-  $entities = \Drupal::entityTypeManager()->getStorage($entity_type)->loadMultiple(array_keys($form['table']['#options']));
-  foreach ($form['table']['#options'] as $entity_id => &$option) {
-    $entity = $entities[$entity_id];
-    $option['title'] = $entity->toLink($entity->label() . ' ' . $entity_type);
+  $data['webform.settings']['test.types'] = Yaml::decode($data['webform.settings']['test.types']);
+  $data['webform.settings']['test.names'] = Yaml::decode($data['webform.settings']['test.names']);
+
+  _webform_lingotek_encode_tokens($data);
+}
+
+/**
+ * Implements hook_lingotek_config_object_translation_presave().
+ */
+function webform_lingotek_config_object_translation_presave(array &$data, $config_name) {
+  if ($config_name !== 'webform.settings') {
+    return;
   }
+
+  _webform_lingotek_decode_tokens($data);
+
+  $data['webform.settings']['test.types'] = Yaml::encode($data['webform.settings']['test.types']);
+  $data['webform.settings']['test.names'] = Yaml::encode($data['webform.settings']['test.names']);
+}
+
+/******************************************************************************/
+// Lingotek decode/encode token functions.
+/******************************************************************************/
+
+/**
+ * Encode all tokens so that they won't be translated.
+ *
+ * @param array $data
+ *   An array of data.
+ */
+function _webform_lingotek_encode_tokens(array &$data) {
+  $yaml = Yaml::encode($data);
+  $yaml = preg_replace_callback(
+    '/\[([a-z][^]]+)\]/',
+    function ($matches) {
+      // Encode all token characters to HTML entities.
+      // @see https://stackoverflow.com/questions/6720826/php-convert-all-characters-to-html-entities.
+      $replacement = mb_encode_numericentity($matches[1], [0x000000, 0x10ffff, 0, 0xffffff], 'UTF-8');
+      return "[$replacement]";
+    },
+    $yaml
+  );
+  $data = Yaml::decode($yaml);
+}
+
+/**
+ * Decode all tokens after string have been translated.
+ *
+ * @param array $data
+ *   An array of data.
+ */
+function _webform_lingotek_decode_tokens(array &$data) {
+  $yaml = Yaml::encode($data);
+  $yaml = preg_replace_callback(
+    '/\[([^]]+?)\]/',
+    function ($matches) {
+      // Decode token HTML entities to characters.
+      // @see https://stackoverflow.com/questions/6720826/php-convert-all-characters-to-html-entities.
+      $token = mb_decode_numericentity($matches[1], [0x000000, 0x10ffff, 0, 0xffffff], 'UTF-8');
+      return "[$token]";
+    },
+    $yaml
+  );
+  $data = Yaml::decode($yaml);
 }
diff --git a/web/modules/webform/js/webform.admin.js b/web/modules/webform/js/webform.admin.js
index 9dc070bd6adda761ae2974d09edfe970823785ec..48408cbf0c796eca9cdfb4588d5133c58e394ee2 100644
--- a/web/modules/webform/js/webform.admin.js
+++ b/web/modules/webform/js/webform.admin.js
@@ -3,7 +3,7 @@
  * JavaScript behaviors for admin pages.
  */
 
-(function ($, Drupal) {
+(function ($, Drupal, debounce) {
 
   'use strict';
 
@@ -42,7 +42,7 @@
     attach: function (context) {
       // Only attach the click event handler to the entire table and determine
       // which row triggers the event.
-      $('.webform-results__table', context).once('webform-results-table').click(function (event) {
+      $('.webform-results-table', context).once('webform-results-table').click(function (event) {
         if (event.target.tagName === 'A' || event.target.tagName === 'BUTTON') {
           return true;
         }
@@ -62,4 +62,4 @@
     }
   };
 
-})(jQuery, Drupal);
+})(jQuery, Drupal, Drupal.debounce);
diff --git a/web/modules/webform/js/webform.admin.tabledrag.js b/web/modules/webform/js/webform.admin.tabledrag.js
new file mode 100644
index 0000000000000000000000000000000000000000..25573770aec84048a9a2044fae950903ba715cac
--- /dev/null
+++ b/web/modules/webform/js/webform.admin.tabledrag.js
@@ -0,0 +1,19 @@
+/**
+ * @file
+ * Dropbutton feature.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  // Make sure that dropButton behavior exists.
+  if (!Drupal.behaviors.tableDrag) {
+    return;
+  }
+
+  $(function () {
+    $('head').append('<style type="text/css">.webform-tabledrag-hide {display: table-cell;}</style>');
+  });
+
+})(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.ajax.js b/web/modules/webform/js/webform.ajax.js
index 1b8b25c9ed1b9aef31384778ff7803fbefb94cd1..da3ac6b13e5e8f0450f35690a13d010815dac479 100644
--- a/web/modules/webform/js/webform.ajax.js
+++ b/web/modules/webform/js/webform.ajax.js
@@ -26,7 +26,7 @@
    */
   Drupal.behaviors.webformAjaxLink = {
     attach: function (context) {
-      $('.webform-ajax-link').once('webform-ajax-link').each(function () {
+      $('.webform-ajax-link', context).once('webform-ajax-link').each(function () {
         var element_settings = {};
         element_settings.progress = {type: 'fullscreen'};
 
@@ -44,21 +44,34 @@
         element_settings.element = this;
         Drupal.ajax(element_settings);
 
-        // For anchor tags with 'data-hash' attribute, add the hash to current
-        // pages location.
-        // @see \Drupal\webform_ui\WebformUiEntityElementsForm::getElementRow
-        // @see Drupal.behaviors.webformFormTabs
-        var hash = $(this).data('hash');
-        if (hash) {
-          $(this).on('click', function() {
-            location.hash = $(this).data('hash');
+        // Close all open modal dialogs when opening off-canvas dialog.
+        if (element_settings.dialogRenderer === 'off_canvas') {
+          $(this).on('click', function () {
+            $('.ui-dialog.webform-ui-dialog:visible').find('.ui-dialog-content').dialog('close');
           });
         }
+      });
+    }
+  };
 
-        // Close all open modal dialogs when opening off-canvas dialog.
-        if (element_settings.dialogRenderer === 'off_canvas') {
-          $(this).on('click', function() {
-            $(".ui-dialog.webform-modal:visible").find('.ui-dialog-content').dialog('close');
+  /**
+   * Adds a hash (#) to current pages location for links and buttons
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches the behavior to a[data-hash] or :button[data-hash].
+   *
+   * @see \Drupal\webform_ui\WebformUiEntityElementsForm::getElementRow
+   * @see Drupal.behaviors.webformFormTabs
+   */
+  Drupal.behaviors.webformAjaxHash = {
+    attach: function (context) {
+      $('[data-hash]', context).once('webform-ajax-hash').each(function () {
+        var hash = $(this).data('hash');
+        if (hash) {
+          $(this).on('click', function () {
+            location.hash = $(this).data('hash');
           });
         }
       });
@@ -77,7 +90,7 @@
     attach: function (context) {
       $('.js-webform-confirmation-back-link-ajax', context)
         .once('webform-confirmation-back-ajax')
-        .click(function(event) {
+        .click(function (event) {
           var $form = $(this).parents('form');
 
           // Trigger the Ajax call back for the hidden submit button.
@@ -98,15 +111,20 @@
     }
   };
 
-  /****************************************************************************/
+  /** ********************************************************************** **/
   // Ajax commands.
-  /****************************************************************************/
+  /** ********************************************************************** **/
 
   /**
    * Track the updated table row key.
    */
   var updateKey;
 
+  /**
+   * Track the add element key.
+   */
+  var addElement;
+
   /**
    * Command to insert new content into the DOM.
    *
@@ -129,37 +147,58 @@
     // Insert the HTML.
     this.insert(ajax, response, status);
 
-    // Scroll to and highlight the updated table row.
-    if (updateKey) {
+    // Add element.
+    if (addElement) {
+      var addSelector = (addElement === '_root_')
+        ? '#webform-ui-add-element'
+        : '[data-drupal-selector="edit-webform-ui-elements-' + addElement + '-add"]';
+      $(addSelector).click();
+    }
+
+    // If not add element, then scroll to and highlight the updated table row.
+    if (!addElement && updateKey) {
       var $element = $('tr[data-webform-key="' + updateKey + '"]');
 
       // Highlight the updated element's row.
       $element.addClass('color-success');
-      setTimeout(function() {$element.removeClass('color-success')}, 3000);
+      setTimeout(function () {$element.removeClass('color-success');}, 3000);
+
+      // Focus first tabbable item for the updated elements and handlers.
+      $element.find(':tabbable:not(.tabledrag-handle)').eq(0).focus();
 
       // Scroll to elements that are not visible.
       if (!isScrolledIntoView($element)) {
         $('html, body').animate({scrollTop: $element.offset().top - Drupal.webform.ajax.scrollTopOffset}, 500);
       }
     }
-    updateKey = null; // Reset element update.
+    else {
+      // Focus main content.
+      $('#main-content').focus();
+    }
 
     // Display main page's status message in a floating container.
     var $wrapper = $(response.selector);
     if ($wrapper.parents('.ui-dialog').length === 0) {
       var $messages = $wrapper.find('.messages');
-      if ($messages.length) {
+      // If 'add element' don't show any messages.
+      if (addElement) {
+        $messages.remove();
+      }
+      else if ($messages.length) {
         var $floatingMessage = $('#webform-ajax-messages');
         if ($floatingMessage.length === 0) {
           $floatingMessage = $('<div id="webform-ajax-messages" class="webform-ajax-messages"></div>');
           $('body').append($floatingMessage);
         }
-        if ($floatingMessage.is(":animated")) {
+        if ($floatingMessage.is(':animated')) {
           $floatingMessage.stop(true, true);
         }
         $floatingMessage.html($messages).show().delay(3000).fadeOut(1000);
       }
     }
+
+    updateKey = null; // Reset element update.
+    addElement = null; // Reset add element.
   };
 
   /**
@@ -188,7 +227,7 @@
       scrollTarget = $(scrollTarget).parent();
     }
 
-    if (response.target == 'page' && $(scrollTarget).length && $(scrollTarget)[0].tagName === 'HTML') {
+    if (response.target === 'page' && $(scrollTarget).length && $(scrollTarget)[0].tagName === 'HTML') {
       // Scroll to top when scroll target is the entire page.
       // @see https://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport
       var rect = $(scrollTarget)[0].getBoundingClientRect();
@@ -202,6 +241,14 @@
         $(scrollTarget).animate({scrollTop: (offset.top - Drupal.webform.ajax.scrollTopOffset)}, 500);
       }
     }
+
+    // Focus on the form wrapper content bookmark if
+    // .js-webform-autofocus is not enabled.
+    // @see \Drupal\webform\Form\WebformAjaxFormTrait::buildAjaxForm
+    var $form = $(response.selector + '-content').find('form');
+    if (!$form.hasClass('js-webform-autofocus')) {
+      $(response.selector + '-content').focus();
+    }
   };
 
   /**
@@ -221,25 +268,39 @@
     // @see https://stackoverflow.com/questions/6944744/javascript-get-portion-of-url-path
     var a = document.createElement('a');
     a.href = response.url;
-    if (a.pathname == window.location.pathname && $('.webform-ajax-refresh').length) {
-      updateKey = (response.url.match(/[\?|&]update=(.*)($|&)/)) ? RegExp.$1 : null;
+    if (a.pathname === window.location.pathname && $('.webform-ajax-refresh').length) {
+      updateKey = (response.url.match(/[?|&]update=([^&]+)($|&)/)) ? RegExp.$1 : null;
+      addElement = (response.url.match(/[?|&]add_element=([^&]+)($|&)/)) ? RegExp.$1 : null;
       $('.webform-ajax-refresh').click();
     }
     else {
+      // Clear unsaved information flag so that the current webform page
+      // can be redirected.
+      // @see Drupal.behaviors.webformUnsaved.clear
+      if (Drupal.behaviors.webformUnsaved) {
+        Drupal.behaviors.webformUnsaved.clear();
+      }
+
+
       this.redirect(ajax, response, status);
     }
   };
 
   /**
-   * Command to close a dialog.
+   * Command to close a off-canvas and modal dialog.
    *
    * If no selector is given, it defaults to trying to close the modal.
    *
    * @param {Drupal.Ajax} [ajax]
+   *   {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
    * @param {object} response
+   *   The response from the Ajax request.
    * @param {string} response.selector
+   *   Selector to use.
    * @param {bool} response.persist
+   *   Whether to persist the dialog element or not.
    * @param {number} [status]
+   *   The HTTP status code.
    */
   Drupal.AjaxCommands.prototype.webformCloseDialog = function (ajax, response, status) {
     if ($('#drupal-off-canvas').length) {
@@ -254,26 +315,48 @@
       var edge = document.documentElement.dir === 'rtl' ? 'left' : 'right';
       var $mainCanvasWrapper = $('[data-off-canvas-main-canvas]');
       $mainCanvasWrapper.css('padding-' + edge, 0);
+
+      // Resize tabs when closing off-canvas system tray.
+      $(window).trigger('resize.tabs');
     }
-    else {
-      // https://stackoverflow.com/questions/15763909/jquery-ui-dialog-check-if-exists-by-instance-method
-      if ($(response.selector).hasClass('ui-dialog-content')) {
-        this.closeDialog(ajax, response, status);
-      }
+
+    // https://stackoverflow.com/questions/15763909/jquery-ui-dialog-check-if-exists-by-instance-method
+    if ($(response.selector).hasClass('ui-dialog-content')) {
+      this.closeDialog(ajax, response, status);
     }
   };
 
-  /****************************************************************************/
+  /**
+   * Triggers audio UAs to read the supplied text.
+   *
+   * @param {Drupal.Ajax} [ajax]
+   *   A {@link Drupal.ajax} object.
+   * @param {object} response
+   *   Ajax response.
+   * @param {string} response.text
+   *   A string to be read by the UA.
+   * @param {string} [response.priority='polite']
+   *   A string to indicate the priority of the message. Can be either
+   *   'polite' or 'assertive'.
+   *
+   * @see Drupal.announce
+   */
+  Drupal.AjaxCommands.prototype.webformAnnounce = function (ajax, response) {
+    // Delay the announcement.
+    setTimeout(function () {Drupal.announce(response.text, response.priority);}, 200);
+  };
+
+  /** ********************************************************************** **/
   // Helper functions.
-  /****************************************************************************/
+  /** ********************************************************************** **/
 
   /**
    * Determine if element is visible in the viewport.
    *
-   * @param element
+   * @param {Element} element
    *   An element.
    *
-   * @returns {boolean}
+   * @return {boolean}
    *   TRUE if element is visible in the viewport.
    *
    * @see https://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling
diff --git a/web/modules/webform/js/webform.announce.js b/web/modules/webform/js/webform.announce.js
new file mode 100644
index 0000000000000000000000000000000000000000..f51998868719fe821227996ece36428b4486d1bf
--- /dev/null
+++ b/web/modules/webform/js/webform.announce.js
@@ -0,0 +1,33 @@
+/**
+ * @file
+ * JavaScript behaviors for announcing changes.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * Provide Webform announce attribute behavior.
+   *
+   * Announces changes using [data-webform-announce] attribute.
+   *
+   * The announce attributes allows FAPI Ajax callbacks to easily
+   * trigger announcements.
+   *
+   * @see \Drupal\webform\Element\WebformComputedBase::ajaxWebformComputedCallback
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches the behavior to [data-webform-announce] attribute.
+   */
+  Drupal.behaviors.webformAnnounce = {
+    attach: function (context) {
+      $('[data-webform-announce]', context).once('data-webform-announce').each(function () {
+        Drupal.announce($(this).data('webform-announce'));
+      });
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.confirmation.modal.js b/web/modules/webform/js/webform.confirmation.modal.js
index d72264c270da707791da97b35a7116ec5ec3931f..8e09d84c66d574900aeb6d62c611fc7288381836 100644
--- a/web/modules/webform/js/webform.confirmation.modal.js
+++ b/web/modules/webform/js/webform.confirmation.modal.js
@@ -19,7 +19,7 @@
    */
   Drupal.behaviors.webformConfirmationModal = {
     attach: function (context) {
-      $('.js-webform-confirmation-modal', context).once('webform-confirmation-modal').each(function() {
+      $('.js-webform-confirmation-modal', context).once('webform-confirmation-modal').each(function () {
         var $element = $(this);
 
         var $dialog = $element.find('.webform-confirmation-modal--content');
@@ -30,6 +30,8 @@
           resizable: false,
           title: $element.find('.webform-confirmation-modal--title').text(),
           close: function (event) {
+            Drupal.dialog(event.target).close();
+            Drupal.detachBehaviors(event.target, null, 'unload');
             $(event.target).remove();
           }
         };
@@ -40,7 +42,15 @@
 
         // Use setTimeout to prevent dialog.position.js
         // Uncaught TypeError: Cannot read property 'settings' of undefined
-        setTimeout(function() {dialog.showModal()}, 1);
+        setTimeout(function () {
+          dialog.showModal();
+
+          // Close any open webform submission modals.
+          var $modal = $('#drupal-modal');
+          if ($modal.find('.webform-submission-form').length) {
+            Drupal.dialog($modal .get(0)).close();
+          }
+        }, 1);
       });
     }
   };
diff --git a/web/modules/webform/js/webform.contextual.js b/web/modules/webform/js/webform.contextual.js
index 157207ec1f3150dc8d17918f478623f5af9e76cd..596c4cd24363b7e7f892d01759d4bdf209cb43ec 100644
--- a/web/modules/webform/js/webform.contextual.js
+++ b/web/modules/webform/js/webform.contextual.js
@@ -11,15 +11,15 @@
   // dynamically inserted via Ajax.
   // @see webform_contextual_links_view_alter()
   // @see Drupal.behaviors.contextual
-  $(document).on('click', '.contextual', function() {
-    $(this).find('a.webform-contextual').once('webform-contextual').each(function() {
+  $(document).on('click', '.contextual', function () {
+    $(this).find('a.webform-contextual').once('webform-contextual').each(function () {
       this.href = this.href.split('?')[0];
 
       // Add ?_webform_test={webform} to the current page's URL.
-      if (/webform\/([^\/]+)\/test/.test(this.href)) {
+      if (/webform\/([^/]+)\/test/.test(this.href)) {
         this.href = window.location.pathname + '?_webform_test=' + RegExp.$1;
       }
     });
   });
-  
+
 })(jQuery);
diff --git a/web/modules/webform/js/webform.dialog.js b/web/modules/webform/js/webform.dialog.js
index d28c2b8168f62626d2b107039c9760e1cf15aa93..99135a692f0cb5b634f8898d8793bccec296a166 100644
--- a/web/modules/webform/js/webform.dialog.js
+++ b/web/modules/webform/js/webform.dialog.js
@@ -1,19 +1,75 @@
 /**
  * @file
- * JavaScript behaviors to fix dialogs.
+ * JavaScript behaviors for webform dialogs.
  */
 
-(function ($, Drupal) {
+(function ($, Drupal, drupalSettings) {
 
   'use strict';
 
-  // @see http://stackoverflow.com/questions/20533487/how-to-ensure-that-ckeditor-has-focus-when-displayed-inside-of-jquery-ui-dialog
-  var _allowInteraction = $.ui.dialog.prototype._allowInteraction;
-  $.ui.dialog.prototype._allowInteraction = function (event) {
-    if ($(event.target).closest('.cke_dialog').length) {
-      return true;
+  // @see http://api.jqueryui.com/dialog/
+  Drupal.webform = Drupal.webform || {};
+  Drupal.webform.dialog = Drupal.webform.dialog || {};
+  Drupal.webform.dialog.options = Drupal.webform.dialog.options || {};
+
+  /**
+   * Open webform dialog using preset options.
+   *
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.webformDialog = {
+    attach: function (context) {
+      $('a.webform-dialog', context).once('webform-dialog').each(function () {
+        var $a = $(this);
+
+        // Get default options.
+        var options = $.extend({}, Drupal.webform.dialog.options);
+
+        // Get preset dialog options.
+        if ($a.attr('class').match(/webform-dialog-([a-z0-9_]+)/)) {
+          var dialogOptionsName = RegExp.$1;
+          if (drupalSettings.webform.dialog.options[dialogOptionsName]) {
+            options = drupalSettings.webform.dialog.options[dialogOptionsName];
+
+            // Unset title.
+            delete options.title;
+          }
+        }
+
+        // Get custom dialog options.
+        if ($(this).data('dialog-options')) {
+          $.extend(options, $(this).data('dialog-options'));
+        }
+
+        var href = $a.attr('href');
+
+        // Replace ENTITY_TYPE and ENTITY_ID placeholders and update the href.
+        // @see webform_page_attachments()
+        if (href.indexOf('?source_entity_type=ENTITY_TYPE&source_entity_id=ENTITY_ID') !== -1) {
+          if (drupalSettings.webform.dialog.entity_type && drupalSettings.webform.dialog.entity_id) {
+            href = href.replace('ENTITY_TYPE', encodeURIComponent(drupalSettings.webform.dialog.entity_type));
+            href = href.replace('ENTITY_ID', encodeURIComponent(drupalSettings.webform.dialog.entity_id));
+          }
+          else {
+            href = href.replace('?source_entity_type=ENTITY_TYPE&source_entity_id=ENTITY_ID', '');
+          }
+          $a.attr('href', href);
+        }
+
+        // Append _webform_dialog=1 to href to trigger Ajax support.
+        // @see \Drupal\webform\WebformSubmissionForm::setEntity
+        href += (href.indexOf('?') === -1 ? '?' : '&') + '_webform_dialog=1';
+
+        var element_settings = {};
+        element_settings.progress = {type: 'fullscreen'};
+        element_settings.url = href;
+        element_settings.event = 'click';
+        element_settings.dialogType = $a.data('dialog-type') || 'modal';
+        element_settings.dialog = options;
+        element_settings.element = this;
+        Drupal.ajax(element_settings);
+      });
     }
-    return _allowInteraction.apply(this, arguments);
   };
 
-})(jQuery, Drupal);
+})(jQuery, Drupal, drupalSettings);
diff --git a/web/modules/webform/js/webform.element.buttons.buttonset.js b/web/modules/webform/js/webform.element.buttons.buttonset.js
deleted file mode 100644
index 52fe14c94e249f3038509401e6cb086d27c18b27..0000000000000000000000000000000000000000
--- a/web/modules/webform/js/webform.element.buttons.buttonset.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * @file
- * JavaScript behaviors for jQuery UI buttons (buttonset) element integration.
- */
-
-(function ($, Drupal) {
-
-  'use strict';
-
-  /**
-   * Create jQuery UI buttons (buttonset) element.
-   *
-   * @type {Drupal~behavior}
-   */
-  Drupal.behaviors.webformButtonsButtonSet = {
-    attach: function (context) {
-      $(context).find('.js-webform-buttons .form-radios, .js-webform-buttons.form-radios, .js-webform-buttons .js-webform-radios').once('webform-buttons').each(function () {
-        var $input = $(this);
-
-        // Remove all div and classes around radios and labels.
-        $input.html($input.find('input[type="radio"], label').removeClass());
-
-        // Create buttonset.
-        $input.buttonset();
-
-        // Disable buttonset.
-        $input.buttonset('option', 'disabled', $input.find('input[type="radio"]:disabled').length);
-
-        // Turn buttonset off/on when the input is disabled/enabled.
-        // @see webform.states.js
-        $input.on('webform:disabled', function () {
-          $input.buttonset('option', 'disabled', $input.find('input[type="radio"]:disabled').length);
-        });
-      });
-    }
-  };
-
-})(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.element.buttons.checkboxradio.js b/web/modules/webform/js/webform.element.buttons.js
similarity index 78%
rename from web/modules/webform/js/webform.element.buttons.checkboxradio.js
rename to web/modules/webform/js/webform.element.buttons.js
index 0c493e4973a4bbaeda083359f74d4d1750e5c6cf..1f7be05bc2ea3bcf2574e04bbdcb498406da80bd 100644
--- a/web/modules/webform/js/webform.element.buttons.checkboxradio.js
+++ b/web/modules/webform/js/webform.element.buttons.js
@@ -17,17 +17,17 @@
   ].join(',');
 
   /**
-   * Create jQuery UI buttons (checkboxradio) element.
+   * Create jQuery UI buttons element.
    *
    * @type {Drupal~behavior}
    */
-  Drupal.behaviors.webformButtonsCheckboxRadio = {
+  Drupal.behaviors.webformButtons = {
     attach: function (context) {
       $(context).find(Drupal.webform.buttons.selector).once('webform-buttons').each(function () {
         var $buttons = $(this);
 
         // Remove classes around radios and labels and move to main element.
-        $buttons.find('input[type="radio"], label').each(function() {
+        $buttons.find('input[type="radio"], label').each(function () {
           $buttons.append($(this).removeAttr('class'));
         });
 
@@ -41,7 +41,7 @@
         var $input = $buttons.find('input[type="radio"]');
 
         // Create checkboxradio.
-        $input.checkboxradio({'icon': false});
+        $input.checkboxradio({icon: false});
 
         // Disable checkboxradio.
         $input.checkboxradio('option', 'disabled', $input.is(':disabled'));
@@ -51,6 +51,15 @@
         $input.on('webform:disabled', function () {
           $input.checkboxradio('option', 'disabled', $input.is(':disabled'));
         });
+
+        // Refresh checkboxradio when input is changed via webform.states.js.
+        // @see webform.states.js ::triggerEventHandlers().
+        $input.on('change', function (event, param1) {
+          if (param1 === 'webform.states') {
+            $input.checkboxradio('refresh');
+          }
+        });
+
       });
     }
   };
diff --git a/web/modules/webform/js/webform.element.chosen.js b/web/modules/webform/js/webform.element.chosen.js
index 25d5d873ef708c53a1040d4b90d0985bde644b6b..3c4c303ac8fb3e54fc190bea788b550ca4c8a1ee 100644
--- a/web/modules/webform/js/webform.element.chosen.js
+++ b/web/modules/webform/js/webform.element.chosen.js
@@ -23,12 +23,53 @@
         return;
       }
 
-      var options = $.extend({width: '100%'}, Drupal.webform.chosen.options);
+      // Add HTML5 required attribute support.
+      // Checking for $oldChosen to prevent duplicate workarounds from
+      // being applied.
+      // @see https://github.com/harvesthq/chosen/issues/515
+      if (!$.fn.oldChosen) {
+        $.fn.oldChosen = $.fn.chosen;
+        $.fn.chosen = function (options) {
+          var select = $(this);
+          var is_creating_chosen = !!options;
+          if (is_creating_chosen && select.css('position') === 'absolute') {
+            select.removeAttr('style');
+          }
+          var ret = select.oldChosen(options);
+          if (is_creating_chosen && select.css('display') === 'none') {
+            select.attr('style', 'display:visible; position:absolute; width:0px; height: 0px; clip:rect(0,0,0,0)');
+            select.attr('tabindex', -1);
+          }
+          return ret;
+        };
+      }
 
       $(context)
         .find('select.js-webform-chosen, .js-webform-chosen select')
         .once('webform-chosen')
-        .chosen(options);
+        .each(function () {
+          var $select = $(this);
+          // Check for .chosen-enable to prevent the chosen.module and
+          // webform.module from both initializing the chosen select element.
+          if ($select.hasClass('chosen-enable')) {
+            return;
+          }
+
+          var options = $.extend({width: '100%'}, Drupal.webform.chosen.options);
+          if ($select.data('placeholder')) {
+            if ($select.prop('multiple')) {
+              options.placeholder_text_multiple = $select.data('placeholder');
+            }
+            else {
+              // Clear option value so that placeholder is displayed.
+              $select.find('option[value=""]').html('');
+              // Allow single option to be deselected.
+              options.allow_single_deselect = true;
+            }
+          }
+
+          $select.chosen(options);
+        });
     }
   };
 
diff --git a/web/modules/webform/js/webform.element.codemirror.js b/web/modules/webform/js/webform.element.codemirror.js
index d711f92bf092a72caccaa8f20d03336eb2da66cf..56d0ff7cc5e2520a0ba9335046e3d9740986c092 100644
--- a/web/modules/webform/js/webform.element.codemirror.js
+++ b/web/modules/webform/js/webform.element.codemirror.js
@@ -38,14 +38,30 @@
         var options = $.extend({
           mode: $(this).attr('data-webform-codemirror-mode'),
           lineNumbers: true,
+          lineWrapping: ($(this).attr('wrap') === 'off') ? false : true,
           viewportMargin: Infinity,
           readOnly: ($(this).prop('readonly') || $(this).prop('disabled')) ? true : false,
-          // Setting for using spaces instead of tabs - https://github.com/codemirror/CodeMirror/issues/988
           extraKeys: {
+            // Setting for using spaces instead of tabs - https://github.com/codemirror/CodeMirror/issues/988
             Tab: function (cm) {
               var spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
               cm.replaceSelection(spaces, 'end', '+element');
+            },
+            // On 'Escape' move to the next tabbable input.
+            // @see http://bgrins.github.io/codemirror-accessible/
+            Esc: function (cm) {
+              // Must show and then textarea so that we can determine
+              // its tabindex.
+              var textarea = $(cm.getTextArea());
+              $(textarea).show().addClass('visually-hidden');
+              var $tabbable = $(':tabbable');
+              var tabindex = $tabbable.index(textarea);
+              $(textarea).hide().removeClass('visually-hidden');
+
+              // Tabindex + 2 accounts for the CodeMirror's iframe.
+              $tabbable.eq(tabindex + 2).focus();
             }
+
           }
         }, Drupal.webform.codeMirror.options);
 
@@ -72,6 +88,27 @@
           editor.setOption('readOnly', $input.is(':disabled'));
         });
 
+        // Delay refreshing CodeMirror for 10 millisecond while the dialog is
+        // still being rendered.
+        // @see http://stackoverflow.com/questions/8349571/codemirror-editor-is-not-loading-content-until-clicked
+        setTimeout(function () {
+          // Show tab panel and open details.
+          var $tabPanel = $input.parents('.ui-tabs-panel:hidden');
+          var $details = $input.parents('details:not([open])');
+
+          if (!$tabPanel.length && $details.length) {
+            return;
+          }
+
+          $tabPanel.show();
+          $details.attr('open', 'open');
+
+          editor.refresh();
+
+          // Hide tab panel and close details.
+          $tabPanel.hide();
+          $details.removeAttr('open');
+        }, 10);
       });
 
       // Webform CodeMirror syntax coloring.
@@ -79,55 +116,7 @@
         // Mode Runner - http://codemirror.net/demo/runmode.html
         CodeMirror.runMode($(this).addClass('cm-s-default').text(), $(this).attr('data-webform-codemirror-mode'), this);
       });
-
     }
   };
 
-  /****************************************************************************/
-  // Refresh functions.
-  /****************************************************************************/
-
-  /**
-   * Refresh codemirror element to make sure it renders correctly.
-   *
-   * @param element
-   *   An element containing a CodeMirror editor.
-   */
-  function refresh(element) {
-    // Show tab panel and open details.
-    var $tabPanel = $(element).parents('.ui-tabs-panel:hidden');
-    $tabPanel.show();
-    var $details = $(element).parents('details:not([open])');
-    $details.attr('open', 'open');
-
-    element.CodeMirror.refresh();
-
-    // Hide tab panel and close details.
-    $tabPanel.hide();
-    $details.removeAttr('open');
-  }
-
-  // Workaround: When a dialog opens we need to reference all CodeMirror
-  // editors to make sure they are properly initialized and sized.
-  $(window).on('dialog:aftercreate', function (dialog, $element, settings) {
-    // Delay refreshing CodeMirror for 10 millisecond while the dialog is
-    // still being rendered.
-    // @see http://stackoverflow.com/questions/8349571/codemirror-editor-is-not-loading-content-until-clicked
-    setTimeout(function () {
-      $('.CodeMirror').each(function (index, element) {
-        refresh(element);
-      });
-    }, 10);
-  });
-
-  // On state:visible refresh CodeMirror elements.
-  $(document).on('state:visible state:visible-slide', function (event) {
-    var $element = $(event.target).parent().find('.js-webform-codemirror');
-    $element.parent().find('.CodeMirror').each(function (index, element) {
-      setTimeout(function () {
-        refresh(element);
-      }, 1);
-    });
-  });
-
 })(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.element.color.js b/web/modules/webform/js/webform.element.color.js
index 3cf9cb39651ec2896d68cba611cb0826695e5239..f5ff08948f0bd21f729a4126e0f18af7a54404be 100644
--- a/web/modules/webform/js/webform.element.color.js
+++ b/web/modules/webform/js/webform.element.color.js
@@ -24,14 +24,21 @@
             .removeClass('form-color-large');
         }
         else {
-          // Display color input's output to the end user.
-          var $output = $('<input class="form-color-output ' + $element.attr('class') + ' js-webform-input-mask" data-inputmask-mask="\\#######" />');
+          // Display color input's output w/ visually-hidden label to
+          // the end user.
+          var $output = $('<input class="form-color-output ' + $element.attr('class') + ' js-webform-input-mask" data-inputmask-mask="\\#######" />').uniqueId();
+          var $label = $element.parent('.js-form-type-color').find('label').clone();
+          $label.attr({
+            for: $output.attr('id'),
+            class: 'visually-hidden'
+          });
           if ($.fn.inputmask) {
             $output.inputmask();
           }
           $output[0].value = $element[0].value;
           $element
             .after($output)
+            .after($label)
             .css({float: 'left'});
 
           // Sync $element and $output.
diff --git a/web/modules/webform/js/webform.element.composite.js b/web/modules/webform/js/webform.element.composite.js
index 58d5a1a28f70414de30399a93881d6be78f1dafb..ac0184db1acdd5a903f45d13878025a80e74022b 100644
--- a/web/modules/webform/js/webform.element.composite.js
+++ b/web/modules/webform/js/webform.element.composite.js
@@ -14,14 +14,14 @@
    */
   Drupal.behaviors.webformElementComposite = {
     attach: function (context) {
-      $('[data-composite-types]').once('webform-composite-types').each(function() {
+      $('[data-composite-types]').once('webform-composite-types').each(function () {
         var $element = $(this);
         var $type = $element.closest('tr').find('.js-webform-composite-type');
 
         var types = $element.attr('data-composite-types').split(',');
         var required = $element.attr('data-composite-required');
 
-        $type.on('change', function() {
+        $type.on('change', function () {
           if ($.inArray($(this).val(), types) === -1) {
             $element.hide();
             if (required) {
@@ -31,11 +31,11 @@
           else {
             $element.show();
             if (required) {
-              $element.attr({ 'required': 'required', 'aria-required': 'true' })
+              $element.attr({'required': 'required', 'aria-required': 'true'});
             }
           }
         }).change();
-      })
+      });
     }
   };
 
diff --git a/web/modules/webform/js/webform.element.computed.js b/web/modules/webform/js/webform.element.computed.js
new file mode 100644
index 0000000000000000000000000000000000000000..1853c1e734e6dbff328006bf152545877e33ef1d
--- /dev/null
+++ b/web/modules/webform/js/webform.element.computed.js
@@ -0,0 +1,74 @@
+/**
+ * @file
+ * JavaScript behaviors for computed elements.
+ */
+
+(function ($, Drupal, debounce) {
+
+  'use strict';
+
+  Drupal.webform = Drupal.webform || {};
+  Drupal.webform.computed = Drupal.webform.computed || {};
+  Drupal.webform.computed.delay = Drupal.webform.computed.delay || 500;
+
+  /**
+   * Initialize computed elements.
+   *
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.webformComputed = {
+    attach: function (context) {
+      $(context).find('.js-webform-computed').once('webform-computed').each(function () {
+        // Get computed element and parent form.
+        var $element = $(this);
+        var $form = $element.closest('form');
+
+        // Get elements that are used by the computed element.
+        var elementKeys = $(this).data('webform-element-keys').split(',');
+        if (!elementKeys) {
+          return;
+        }
+
+        // Get computed element triggers.
+        var inputs = [];
+        $.each(elementKeys, function (i, key) {
+          // Exact input match.
+          inputs.push(':input[name="' + key + '"]');
+          // Sub inputs. (aka #tree)
+          inputs.push(':input[name^="' + key + '["]');
+        });
+        var $triggers = $form.find(inputs.join(','));
+
+        // Add event handler to computed element triggers.
+        $triggers.on('keyup change',
+          debounce(triggerUpdate, Drupal.webform.computed.delay));
+
+        // Initialize computed element update which refreshes the displayed
+        // value and accounts for any changes to the #default_value for a
+        // computed element.
+        triggerUpdate(true);
+
+        function triggerUpdate(initialize) {
+          // Prevent duplicate computations.
+          // @see Drupal.behaviors.formSingleSubmit
+          if (initialize !== true) {
+            var formValues = $triggers.serialize();
+            var previousValues = $element.attr('data-webform-computed-last');
+            if (previousValues === formValues) {
+              return;
+            }
+            $element.attr('data-webform-computed-last', formValues);
+          }
+
+          // Add loading class to computed wrapper.
+          $element.find('.js-webform-computed-wrapper')
+            .addClass('webform-computed-loading');
+
+          // Trigger computation.
+          $element.find('.js-form-submit').mousedown();
+        }
+      });
+    }
+  };
+
+})(jQuery, Drupal, Drupal.debounce);
diff --git a/web/modules/webform/js/webform.element.counter.js b/web/modules/webform/js/webform.element.counter.js
index 701163943675f56c4be117fee41602f0275b9dbf..903ba2369c3052a780144c424ea79adf3dadeb19 100644
--- a/web/modules/webform/js/webform.element.counter.js
+++ b/web/modules/webform/js/webform.element.counter.js
@@ -1,13 +1,13 @@
 /**
  * @file
- * JavaScript behaviors for jQuery Word and Counter Counter integration.
+ * JavaScript behaviors for jQuery Text Counter integration.
  */
 
 (function ($, Drupal) {
 
   'use strict';
 
-  // @see http://qwertypants.github.io/jQuery-Word-and-Character-Counter-Plugin/
+  // @see https://github.com/ractoon/jQuery-Text-Counter#options
   Drupal.webform = Drupal.webform || {};
   Drupal.webform.counter = Drupal.webform.counter || {};
   Drupal.webform.counter.options = Drupal.webform.counter.options || {};
@@ -19,29 +19,38 @@
    */
   Drupal.behaviors.webformCounter = {
     attach: function (context) {
-      if (!$.fn.counter) {
+      if (!$.fn.textcounter) {
         return;
       }
 
       $(context).find('.js-webform-counter').once('webform-counter').each(function () {
         var options = {
-          goal: $(this).attr('data-counter-limit'),
-          msg: $(this).attr('data-counter-message')
+          type: $(this).data('counter-type'),
+          max: $(this).data('counter-maximum'),
+          min: $(this).data('counter-minimum') || 0,
+          counterText: $(this).data('counter-minimum-message'),
+          countDownText: $(this).data('counter-maximum-message'),
+          inputErrorClass: 'webform-counter-warning',
+          counterErrorClass: 'webform-counter-warning',
+          countSpaces: true,
+          stopInputAtMaximum: false,
+          // Don't display min/max message since server-side validation will
+          // display these messages.
+          minimumErrorText: '',
+          maximumErrorText: ''
         };
 
-        // Only word type can be defined, otherwise the counter defaults to
-        // character counting.
-        if ($(this).attr('data-counter-type') === 'word') {
-          options.type = 'word';
+        options.countDown = (options.max) ? true : false;
+        if (!options.counterText) {
+          options.counterText = (options.type === 'word') ? Drupal.t('%d word(s) entered') : Drupal.t('%d characters(s) entered');
+        }
+        if (!options.countDownText) {
+          options.countDownText = (options.type === 'word') ? Drupal.t('%d word(s) remaining') : Drupal.t('%d characters(s) remaining');
         }
 
         options = $.extend(options, Drupal.webform.counter.options);
 
-        // Set the target to a div that is appended to end of the input's parent container.
-        options.target = $('<div class="webform-counter-message"></div>');
-        $(this).parent().append(options.target);
-
-        $(this).counter(options);
+        $(this).textcounter(options);
       });
 
     }
diff --git a/web/modules/webform/js/webform.element.date.js b/web/modules/webform/js/webform.element.date.js
index 0af0042fb9a69c442e90aa9f88b8e0bf31572023..b091a80f2dee7efe3524ded49355094b0834904c 100644
--- a/web/modules/webform/js/webform.element.date.js
+++ b/web/modules/webform/js/webform.element.date.js
@@ -32,7 +32,7 @@
 
         // Skip if date inputs are supported by the browser and input is not a text field.
         // @see \Drupal\webform\Element\WebformDatetime
-        if (window.Modernizr && Modernizr.inputtypes.date === true && $input.attr('type') !== 'text') {
+        if (window.Modernizr && Modernizr.inputtypes && Modernizr.inputtypes.date === true && $input.attr('type') !== 'text') {
           return;
         }
 
@@ -41,6 +41,16 @@
           changeYear: true
         }, Drupal.webform.datePicker.options);
 
+        // Add datepicker button.
+        if ($input.hasData('datepicker-button')) {
+          options = $.extend({
+            showOn: 'both',
+            buttonImage: settings.webform.datePicker.buttonImage,
+            buttonImageOnly: true,
+            buttonText: Drupal.t('Select date')
+          }, Drupal.webform.datePicker.options);
+        }
+
         var dateFormat = $input.data('drupalDateFormat');
 
         // The date format is saved in PHP style, we need to convert to jQuery
@@ -48,7 +58,7 @@
         // @see http://stackoverflow.com/questions/16702398/convert-a-php-date-format-to-a-jqueryui-datepicker-date-format
         // @see http://php.net/manual/en/function.date.php
         options.dateFormat = dateFormat
-          // Year.
+        // Year.
           .replace('Y', 'yy') // A full numeric representation of a year, 4 digits (1999 or 2003)
           .replace('y', 'y') // A two digit representation of a year (99 or 03)
           // Month.
@@ -72,20 +82,30 @@
 
         // Add min/max year to data range.
         if (!options.yearRange && $input.data('min-year') && $input.data('max-year')) {
-          options.yearRange = $input.data('min-year') + ':' + $input.attr('data-max-year')
+          options.yearRange = $input.data('min-year') + ':' + $input.attr('data-max-year');
         }
 
         // First day of the week.
         options.firstDay = settings.webform.dateFirstDay;
 
+        // Disable autocomplete.
+        // @see https://gist.github.com/niksumeiko/360164708c3b326bd1c8
+        var isChrome = /Chrome/.test(window.navigator.userAgent) && /Google Inc/.test(window.navigator.vendor);
+        $input.attr('autocomplete', (isChrome) ? 'off' : 'false');
+
         $input.datepicker(options);
       });
+    }
+    // Issue #2983363: Datepicker is being detached when multiple files are
+    // uploaded.
+    /*
     },
     detach: function (context, settings, trigger) {
       if (trigger === 'unload') {
         $(context).find('input[data-drupal-date-format]').findOnce('datePicker').datepicker('destroy');
       }
     }
+    */
   };
 
 })(jQuery, Modernizr, Drupal);
diff --git a/web/modules/webform/js/webform.element.details.save.js b/web/modules/webform/js/webform.element.details.save.js
index c2e005c84e876166925634a791d1b537ca55c8c9..3fb4a91d6bbf2a0770652fd98eb5c343c393245e 100644
--- a/web/modules/webform/js/webform.element.details.save.js
+++ b/web/modules/webform/js/webform.element.details.save.js
@@ -68,7 +68,7 @@
    * @param {jQuery} $details
    *   A details element.
    *
-   * @return string
+   * @return {string}
    *   The name used to store the state of details element.
    */
   Drupal.webformDetailsSaveGetName = function ($details) {
@@ -103,6 +103,6 @@
     formId = formId.replace(/--.+?$/, '').replace(/-/g, '_');
     detailsId = detailsId.replace(/--.+?$/, '').replace(/-/g, '_');
     return 'Drupal.webform.' + formId + '.' + detailsId;
-  }
+  };
 
 })(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.element.details.toggle.js b/web/modules/webform/js/webform.element.details.toggle.js
index 0041e641fd28fb970939b1c949040733f00ac9b0..550f32a5f30ad141a19a3b34c9d87fd0ea65d2d1 100644
--- a/web/modules/webform/js/webform.element.details.toggle.js
+++ b/web/modules/webform/js/webform.element.details.toggle.js
@@ -20,15 +20,12 @@
     attach: function (context) {
       $('.js-webform-details-toggle', context).once('webform-details-toggle').each(function () {
         var $form = $(this);
+        var $tabs = $form.find('.webform-tabs');
 
-        var options = $.extend({
-          'button': '<button type="button" class="link webform-details-toggle-state"></button>'
-        }, Drupal.webform.detailsToggle.options);
-
-        // Get only the main details elements and ingnore all nested details.
-        var $details = $form.find('details').filter(function() {
-          // @todo Figure out how to optimize the below code.
-          var $parents = $(this).parentsUntil('.js-webform-details-toggle');
+        // Get only the main details elements and ignore all nested details.
+        var selector = ($tabs.length) ? '.webform-tab' : '.js-webform-details-toggle';
+        var $details = $form.find('details').filter(function () {
+          var $parents = $(this).parentsUntil(selector);
           return ($parents.find('details').length === 0);
         });
 
@@ -37,8 +34,12 @@
           return;
         }
 
-        // Add toggle state link to first details element.
-        $details.first().before($(options.button)
+        var options = $.extend({
+          button: '<button type="button" class="webform-details-toggle-state"></button>'
+        }, Drupal.webform.detailsToggle.options);
+
+        // Create toggle buttons.
+        var $toggle = $(options.button)
           .attr('title', Drupal.t('Toggle details widget state.'))
           .on('click', function (e) {
             var open;
@@ -64,8 +65,16 @@
             }
           })
           .wrap('<div class="webform-details-toggle-state-wrapper"></div>')
-          .parent()
-        );
+          .parent();
+
+        if ($tabs.length) {
+          // Add toggle state before the tabs.
+          $tabs.find('.item-list:first-child').eq(0).before($toggle);
+        }
+        else {
+          // Add toggle state link to first details element.
+          $details.eq(0).before($toggle);
+        }
 
         setDetailsToggleLabel($form);
       });
@@ -92,8 +101,13 @@
    *   A webform.
    */
   function setDetailsToggleLabel($form) {
-    var label = (isFormDetailsOpen($form)) ? Drupal.t('Collapse all') : Drupal.t('Expand all');
+    var isOpen = isFormDetailsOpen($form);
+
+    var label = (isOpen) ? Drupal.t('Collapse all') : Drupal.t('Expand all');
     $form.find('.webform-details-toggle-state').html(label);
+
+    var text = (isOpen) ? Drupal.t('All details have been expanded.') : Drupal.t('All details have been collapsed.');
+    Drupal.announce(text);
   }
 
 })(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.element.help.js b/web/modules/webform/js/webform.element.help.js
index b8337c9c83d460c902da9a156bb1f74e50923d33..fc43577a59b3fdadea5b9b9d6d892c4dd9a9938d 100644
--- a/web/modules/webform/js/webform.element.help.js
+++ b/web/modules/webform/js/webform.element.help.js
@@ -11,19 +11,19 @@
   Drupal.webform = Drupal.webform || {};
   Drupal.webform.elementHelpIcon = Drupal.webform.elementHelpIcon || {};
   Drupal.webform.elementHelpIcon.options = Drupal.webform.elementHelpIcon.options || {
-    position: { my: "left+5 top+5", at: "left bottom", collision: "flipfit" },
+    position: {my: 'left+5 top+5', at: 'left bottom', collision: 'flipfit'},
     tooltipClass: 'webform-element-help--tooltip',
     // @see https://stackoverflow.com/questions/18231315/jquery-ui-tooltip-html-with-links
-    show: null,
+    show: {delay: 100},
     close: function (event, ui) {
       ui.tooltip.hover(
         function () {
           $(this).stop(true).fadeTo(400, 1);
         },
         function () {
-          $(this).fadeOut("400", function () {
+          $(this).fadeOut('400', function () {
             $(this).remove();
-          })
+          });
         });
     }
   };
@@ -38,12 +38,39 @@
       $(context).find('.webform-element-help').once('webform-element-help').each(function () {
         var $link = $(this);
 
-        var options = $.extend({}, Drupal.webform.elementHelpIcon.options);
-        // Use 'data-webform-help' attribute which can include HTML markup.
-        options.content = $(this).attr('data-webform-help');
-        $link.tooltip(options).on('click', function (event) {
-          event.preventDefault();
-        });
+        var options = $.extend({
+          // Use 'data-webform-help' attribute which can include HTML markup.
+          content: $link.attr('data-webform-help'),
+          items: '[data-webform-help]'
+        }, Drupal.webform.elementHelpIcon.options);
+
+        $link.tooltip(options)
+          .on('click', function (event) {
+            // Prevent click from toggling <label>s wrapped around help.
+            event.preventDefault();
+          }).on('keydown', function(event) {
+            // Prevent ESC from from closing dialogs.
+            if (event.keyCode === $.ui.keyCode.ESCAPE) {
+              event.stopPropagation();
+            }
+          });
+
+        // Help tooltips are generally placed with <label> tags.
+        // Screen readers are also reading the <label> and the
+        // 'aria-describedby' attribute.
+        // To prevent this issue we are removing the <label>'s 'for' attribute
+        // when the tooltip is focused.
+        var $label = $(this).parent('label');
+        var labelFor = $label.attr('for') || '';
+        if ($label.length && labelFor) {
+          $link
+            .on('focus', function () {
+              $label.removeAttr('for');
+            })
+            .on('blur', function () {
+              $label.attr('for', labelFor);
+            });
+        }
       });
     }
   };
diff --git a/web/modules/webform/js/webform.element.html_editor.js b/web/modules/webform/js/webform.element.html_editor.js
index 09a32d507f636c2d57462c61d2febadf4c1442c8..398c4a88dcba1ca91138ca62ca1af36a455d1ee4 100644
--- a/web/modules/webform/js/webform.element.html_editor.js
+++ b/web/modules/webform/js/webform.element.html_editor.js
@@ -41,11 +41,13 @@
           && drupalSettings.yamlEditor.source
           && drupalSettings.yamlEditor.source.indexOf('noconflict') !== -1) {
           delete plugins.codemirror;
-          ('console' in window) && window.console.log('YAML Editor module is not compatible with the ckeditor.codemirror plugin. @see Issue #2936147: ckeditor.codemirror plugin breaks admin textarea.');
+          if ('console' in window) {
+            window.console.log('YAML Editor module is not compatible with the ckeditor.codemirror plugin. @see Issue #2936147: ckeditor.codemirror plugin breaks admin textarea.');
+          }
         }
 
         for (var plugin_name in plugins) {
-          if(plugins.hasOwnProperty(plugin_name)) {
+          if (plugins.hasOwnProperty(plugin_name)) {
             CKEDITOR.plugins.addExternal(plugin_name, plugins[plugin_name]);
           }
         }
@@ -114,7 +116,7 @@
 
         // Catch and suppress
         // "Uncaught TypeError: Cannot read property 'getEditor' of undefined".
-        // 
+        //
         // Steps to reproduce this error.
         // - Goto any form elements.
         // - Edit an element.
diff --git a/web/modules/webform/js/webform.element.icheck.js b/web/modules/webform/js/webform.element.icheck.js
index 59077fbdba1ee3394bad8920db9f6916fdbddc69..4ba85fade5e6ada8a8932f3e269ffe3ff2814f35 100644
--- a/web/modules/webform/js/webform.element.icheck.js
+++ b/web/modules/webform/js/webform.element.icheck.js
@@ -12,17 +12,6 @@
   Drupal.webform.iCheck = Drupal.webform.iCheck || {};
   Drupal.webform.iCheck.options = Drupal.webform.iCheck.options || {};
 
-  function initializeCheckbox($input, options) {
-    $input.addClass('js-webform-icheck')
-      .iCheck(options)
-      // @see https://github.com/fronteed/iCheck/issues/244
-      .on('ifChecked', function (e) {
-        $(e.target).attr('checked', 'checked').change();
-      })
-      .on('ifUnchecked', function (e) {
-        $(e.target).removeAttr('checked').change();
-      });
-  }
   /**
    * Enhance checkboxes and radios using iCheck.
    *
@@ -92,12 +81,12 @@
 
           $(this).iCheck(options);
         })
-        .on('ifChanged', function () {
-          var _index = $(this).parents('th').index() + 1;
-          $(this).parents('thead').next('tbody').find('tr td:nth-child(' + _index + ') input')
-            .iCheck(!$(this).is(':checked') ? 'check' : 'uncheck')
-            .iCheck($(this).is(':checked') ? 'check' : 'uncheck');
-        });
+          .on('ifChanged', function () {
+            var _index = $(this).parents('th').index() + 1;
+            $(this).parents('thead').next('tbody').find('tr td:nth-child(' + _index + ') input')
+              .iCheck(!$(this).is(':checked') ? 'check' : 'uncheck')
+              .iCheck($(this).is(':checked') ? 'check' : 'uncheck');
+          });
       });
     }
   };
diff --git a/web/modules/webform/js/webform.element.inputhide.js b/web/modules/webform/js/webform.element.inputhide.js
new file mode 100644
index 0000000000000000000000000000000000000000..c6cd555a811b93fb47045a3655d1cc2f0a5814ff
--- /dev/null
+++ b/web/modules/webform/js/webform.element.inputhide.js
@@ -0,0 +1,51 @@
+/**
+ * @file
+ * JavaScript behaviors for input hiding.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  var isChrome = /Chrome/.test(window.navigator.userAgent) && /Google Inc/.test(window.navigator.vendor);
+
+  /**
+   * Initialize input hiding.
+   *
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.webformInputHide = {
+    attach: function (context) {
+      // Apply chrome fix to prevent password input from being autofilled.
+      // @see https://stackoverflow.com/questions/15738259/disabling-chrome-autofill
+      if (isChrome) {
+        $(context).find('form:has(input.js-webform-input-hide)')
+          .once('webform-input-hide-chrome-workaround')
+          .each(function () {
+            $(this).prepend('<input style="display:none" type="text" name="chrome_autocomplete_username"/><input style="display:none" type="password" name="chrome_autocomplete_password"/>');
+          });
+      }
+
+      // Convert text based inputs to password input on blur.
+      $(context).find('input.js-webform-input-hide')
+        .once('webform-input-hide')
+        .each(function () {
+          var type = this.type;
+          // Initialize input hiding.
+          this.type = 'password';
+
+          // Attach blur and focus event handlers.
+          $(this)
+            .on('blur', function () {
+              this.type = 'password';
+              $(this).attr('autocomplete', 'off');
+            })
+            .on('focus', function () {
+              this.type = type;
+              $(this).removeAttr('autocomplete');
+            });
+        });
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.element.location.js b/web/modules/webform/js/webform.element.location.geocomplete.js
similarity index 61%
rename from web/modules/webform/js/webform.element.location.js
rename to web/modules/webform/js/webform.element.location.geocomplete.js
index bb66d6e2bec8a67981f45ccd6b1ba723c85ca004..d428f0f76dd4a5e0a4dcceaa34406b26b031d18b 100644
--- a/web/modules/webform/js/webform.element.location.js
+++ b/web/modules/webform/js/webform.element.location.geocomplete.js
@@ -3,7 +3,7 @@
  * JavaScript behaviors for Geocomplete location integration.
  */
 
-(function ($, Drupal, drupalSettings) {
+(function ($, Drupal) {
 
   'use strict';
 
@@ -14,7 +14,7 @@
   Drupal.webform.locationGeocomplete.options = Drupal.webform.locationGeocomplete.options || {};
 
   /**
-   * Initialize location Geocompletion.
+   * Initialize location geocomplete.
    *
    * @type {Drupal~behavior}
    */
@@ -24,19 +24,23 @@
         return;
       }
 
-      $(context).find('div.js-form-type-webform-location').once('webform-location').each(function () {
+      $(context).find('.js-webform-type-webform-location-geocomplete').once('webform-location-geocomplete').each(function () {
         var $element = $(this);
         var $input = $element.find('.webform-location-geocomplete');
+
+        // Display a map.
         var $map = null;
-        if ($input.attr('data-webform-location-map')) {
-          $map = $('<div class="webform-location-map"><div class="webform-location-map--container"></div></div>').insertAfter($input).find('.webform-location-map--container');
+        if ($input.attr('data-webform-location-geocomplete-map')) {
+          $map = $('<div class="webform-location-geocomplete-map"><div class="webform-location-geocomplete-map--container"></div></div>').insertAfter($input).find('.webform-location-geocomplete-map--container');
         }
 
         var options = $.extend({
           details: $element,
-          detailsAttribute: 'data-webform-location-attribute',
+          detailsAttribute: 'data-webform-location-geocomplete-attribute',
           types: ['geocode'],
           map: $map,
+          geocodeAfterResult: false,
+          restoreValueAfterBlur: true,
           mapOptions: {
             disableDefaultUI: true,
             zoomControl: true
@@ -45,26 +49,13 @@
 
         var $geocomplete = $input.geocomplete(options);
 
-        $geocomplete.on('input', function () {
-          // Reset attributes on input.
-          $element.find('[data-webform-location-attribute]').val('');
-        }).on('blur', function () {
-          // Make sure to get attributes on blur.
-          if ($element.find('[data-webform-location-attribute="location"]').val() === '') {
-            var value = $geocomplete.val();
-            if (value) {
-              $geocomplete.geocomplete('find', value);
-            }
-          }
-        });
-
         // If there is default value look up location's attributes, else see if
         // the default value should be set to the browser's current geolocation.
         var value = $geocomplete.val();
         if (value) {
           $geocomplete.geocomplete('find', value);
         }
-        else if (navigator.geolocation && $geocomplete.attr('data-webform-location-geolocation')) {
+        else if (navigator.geolocation && $geocomplete.attr('data-webform-location-geocomplete-geolocation')) {
           navigator.geolocation.getCurrentPosition(function (position) {
             $geocomplete.geocomplete('find', position.coords.latitude + ', ' + position.coords.longitude);
           });
@@ -73,4 +64,4 @@
     }
   };
 
-})(jQuery, Drupal, drupalSettings);
+})(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.element.location.places.js b/web/modules/webform/js/webform.element.location.places.js
new file mode 100644
index 0000000000000000000000000000000000000000..336b31549b0104c0ddb618dba88ac9e5fcb37a3b
--- /dev/null
+++ b/web/modules/webform/js/webform.element.location.places.js
@@ -0,0 +1,101 @@
+/**
+ * @file
+ * JavaScript behaviors for Algolia places location integration.
+ */
+
+(function ($, Drupal, drupalSettings) {
+
+  'use strict';
+
+  // @see https://github.com/algolia/places
+  // @see https://community.algolia.com/places/documentation.html#options
+  Drupal.webform = Drupal.webform || {};
+  Drupal.webform.locationPlaces = Drupal.webform.locationPlaces || {};
+  Drupal.webform.locationPlaces.options = Drupal.webform.locationPlaces.options || {};
+
+  var mapping = {
+    lat: 'lat',
+    lng: 'lng',
+    name: 'name',
+    postcode: 'postcode',
+    locality: 'locality',
+    city: 'city',
+    administrative: 'administrative',
+    country: 'country',
+    countryCode: 'country_code',
+    county: 'county',
+    suburb: 'suburb'
+  };
+
+  /**
+   * Initialize location places.
+   *
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.webformLocationPlaces = {
+    attach: function (context) {
+      if (!window.places) {
+        return;
+      }
+
+      $(context).find('.js-webform-type-webform-location-places').once('webform-location-places').each(function () {
+        var $element = $(this);
+        var $input = $element.find('.webform-location-places');
+
+        // Prevent the 'Enter' key from submitting the form.
+        $input.keydown(function (event) {
+          if (event.keyCode === 13) {
+            event.preventDefault();
+          }
+        });
+
+        var options = $.extend({
+          type: 'address',
+          useDeviceLocation: true,
+          container: $input.get(0)
+        }, Drupal.webform.locationPlaces.options);
+
+        // Add application id and API key.
+        if (drupalSettings.webform.location.places.app_id && drupalSettings.webform.location.places.api_key) {
+          options.appId = drupalSettings.webform.location.places.app_id;
+          options.apiKey = drupalSettings.webform.location.places.api_key;
+        }
+
+        var placesAutocomplete = window.places(options);
+
+        // Disable autocomplete.
+        // @see https://gist.github.com/niksumeiko/360164708c3b326bd1c8
+        var isChrome = /Chrome/.test(window.navigator.userAgent) && /Google Inc/.test(window.navigator.vendor);
+        $input.attr('autocomplete', (isChrome) ? 'off' : 'false');
+
+        // Sync values on change and clear events.
+        placesAutocomplete.on('change', function (e) {
+          $.each(mapping, function (source, destination) {
+            var value = (source === 'lat' || source === 'lng' ? e.suggestion.latlng[source] : e.suggestion[source]) || '';
+            setValue(destination, value);
+          });
+        });
+        placesAutocomplete.on('clear', function (e) {
+          $.each(mapping, function (source, destination) {
+            setValue(destination, '');
+          });
+        });
+
+        /**
+         * Set attribute value.
+         *
+         * @param {string} name
+         *   The attribute name
+         * @param {string} value
+         *   The attribute value
+         */
+        function setValue(name, value) {
+          var inputSelector = ':input[data-webform-location-places-attribute="' + name + '"]';
+          $element.find(inputSelector).val(value);
+        }
+      });
+    }
+  };
+
+})(jQuery, Drupal, drupalSettings);
+
diff --git a/web/modules/webform/js/webform.element.managed_file.js b/web/modules/webform/js/webform.element.managed_file.js
new file mode 100644
index 0000000000000000000000000000000000000000..1628e9ddf860b1a396ce963a83f8463ea0ad99b0
--- /dev/null
+++ b/web/modules/webform/js/webform.element.managed_file.js
@@ -0,0 +1,109 @@
+/**
+ * @file
+ * JavaScript behaviors for managed file uploads.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * Track file uploads and display confirm dialog when an file upload is inprogress.
+   *
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.webformManagedFileAutoUpload = {
+    attach: function attach(context) {
+      // Add submit handler to file upload form.
+      $(context).find('form')
+        .once('webform-auto-file-upload')
+        .on('submit', function (event) {
+          var $form = $(this);
+          if ($form.data('webform-auto-file-uploads') > 0 && blockSubmit($form)) {
+            event.preventDefault();
+            return false;
+          }
+          return true;
+        });
+
+      // Add submit handler to form.beforeSend.
+      // Update Drupal.Ajax.prototype.beforeSend only once.
+      if (typeof Drupal.Ajax !== 'undefined' && typeof Drupal.Ajax.prototype.beforeSubmitWebformManagedFileAutoUploadOriginal === 'undefined') {
+        Drupal.Ajax.prototype.beforeSubmitWebformManagedFileAutoUploadOriginal = Drupal.Ajax.prototype.beforeSubmit;
+        Drupal.Ajax.prototype.beforeSubmit = function (form_values, element_settings, options) {
+          var $form = this.$form;
+          var $element = $(this.element);
+
+          // Determine if the triggering element is within .form-actions.
+          var isFormActions = $element
+            .closest('.form-actions').length;
+
+          // Determine if the triggering element is within a multiple element.
+          var isMultipleUpload = $element
+            .parents('.js-form-type-webform-multiple, .js-form-type-webform-custom-composite')
+            .find('.js-form-managed-file').length;
+
+          // Determine if the triggering element is not within a
+          // managed file element.
+          var isManagedUploadButton = $element.parents('.js-form-managed-file').length;
+
+          // Only trigger block submit for .form-actions and multiple element
+          // with file upload.
+          if ($form.data('webform-auto-file-uploads') > 0 &&
+            (isFormActions || (isMultipleUpload && !isManagedUploadButton)) &&
+            blockSubmit($form)) {
+            this.ajaxing = false;
+            return false;
+          }
+          return this.beforeSubmitWebformManagedFileAutoUploadOriginal();
+        };
+      }
+
+      $(context).find('input[type="file"]').once('webform-auto-file-upload').on('change', function () {
+        // Track file upload.
+        $(this).data('msk-auto-file-upload', true);
+
+        // Increment form file uploads.
+        var $form = $(this.form);
+        var fileUploads = ($form.data('webform-auto-file-uploads') || 0);
+        $form.data('webform-auto-file-uploads', fileUploads + 1);
+      });
+    },
+    detach: function detach(context, settings, trigger) {
+      if (trigger === 'unload') {
+        $(context).find('input[type="file"]').removeOnce('webform-auto-file-upload').each(function () {
+          if ($(this).data('msk-auto-file-upload')) {
+            // Remove file upload tracking.
+            $(this).removeData('msk-auto-file-upload');
+
+            // Decrease form file uploads.
+            var $form = $(this.form);
+            var fileUploads = ($form.data('webform-auto-file-uploads') || 0);
+            $form.data('webform-auto-file-uploads', fileUploads - 1);
+          }
+        });
+      }
+    }
+  };
+
+  /**
+   * Block form submit.
+   *
+   * @param {jQuery} form
+   *   A form.
+   *
+   * @return {boolean}
+   *   TRUE if form submit should be blocked.
+   */
+  function blockSubmit(form) {
+    if ($(form).data('webform-auto-file-uploads') < 0) {
+      return false;
+    }
+
+    var message = Drupal.t('File upload inprogress. Uploaded file may be lost.') +
+      '\n' +
+      Drupal.t('Do you want to continue?');
+    return !window.confirm(message);
+  }
+
+})(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.element.more.js b/web/modules/webform/js/webform.element.more.js
index 44b225c4d10ac3ced1a53d1e0cca5250c6109fda..739bafd832e28799d7f83589621441e2c5bceabe 100644
--- a/web/modules/webform/js/webform.element.more.js
+++ b/web/modules/webform/js/webform.element.more.js
@@ -16,11 +16,42 @@
     attach: function (context) {
       $(context).find('.js-webform-element-more').once('webform-element-more').each(function (event) {
         var $more = $(this);
-        $more.find('a').on('click', function() {
-          $more.toggleClass('is-open');
-          $more.find('.webform-element-more--content').slideToggle();
+        var $a = $more.find('a');
+        var $content = $more.find('.webform-element-more--content');
+
+        // Add aria-* attributes.
+        $a.attr({
+          'aria-expanded': false,
+          'aria-controls': $content.attr('id')
+        });
+
+        // Add event handlers.
+        $a.on('click', toggle)
+          .on('keydown', function (event) {
+            // Space or Return.
+            if (event.which === 32 || event.which === 13) {
+              toggle(event);
+            }
+          });
+
+        function toggle(event) {
+          var expanded = ($a.attr('aria-expanded') === 'true');
+
+          // Toggle `aria-expanded` attributes on link.
+          $a.attr('aria-expanded', !expanded);
+
+          // Toggle content and more .is-open state.
+          if (expanded) {
+            $more.removeClass('is-open');
+            $content.slideUp();
+          }
+          else {
+            $more.addClass('is-open');
+            $content.slideDown();
+          }
+
           event.preventDefault();
-        })
+        }
       });
     }
   };
diff --git a/web/modules/webform/js/webform.element.multiple.js b/web/modules/webform/js/webform.element.multiple.js
index 01787c322fd08f974bec700edd63a75837204f6f..8923a201e382df683ff8bbd3aa623f4ef408d0c1 100644
--- a/web/modules/webform/js/webform.element.multiple.js
+++ b/web/modules/webform/js/webform.element.multiple.js
@@ -1,6 +1,6 @@
 /**
  * @file
- * JavaScript behaviors for message element integration.
+ * JavaScript behaviors for multiple element.
  */
 
 (function ($, Drupal) {
@@ -12,17 +12,42 @@
    *
    * @type {Drupal~behavior}
    */
-  Drupal.behaviors.webformMultiple = {
+  Drupal.behaviors.webformMultipleTableDrag = {
     attach: function (context, settings) {
       for (var base in settings.tableDrag) {
         if (settings.tableDrag.hasOwnProperty(base)) {
-          var $tableDrag = $(context).find('#' + base);
-          var $toggleWeight = $tableDrag.parent().find('.tabledrag-toggle-weight');
-          $toggleWeight.addClass('webform-multiple-tabledrag-toggle-weight');
-          $tableDrag.after($toggleWeight);
+          $(context).find('.js-form-type-webform-multiple #' + base).once('webform-multiple-table-drag').each(function () {
+            var $tableDrag = $(this);
+            var $toggleWeight = $tableDrag.prev().prev('.tabledrag-toggle-weight-wrapper');
+            if ($toggleWeight.length) {
+              $toggleWeight.addClass('webform-multiple-tabledrag-toggle-weight');
+              $tableDrag.after($toggleWeight);
+            }
+          });
         }
       }
     }
   };
 
+  /**
+   * Submit multiple add number input value when enter is pressed.
+   *
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.webformMultipleAdd = {
+    attach: function (context, settings) {
+      $(context).find('.js-webform-multiple-add').once('webform-multiple-add').each(function () {
+        var $submit = $(this).find('input[type="submit"], button');
+        var $number = $(this).find('input[type="number"]');
+        $number.keyup(function (event) {
+          if (event.which === 13) {
+            // Note: Mousedown is the default trigger for Ajax events.
+            // @see Drupal.Ajax.
+            $submit.mousedown();
+          }
+        });
+      });
+    }
+  };
+
 })(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.element.options.admin.js b/web/modules/webform/js/webform.element.options.admin.js
index 0d1071eb79decf9a71d88a98b8b27faa41a89b35..0a1db52b4e2c75d30901f56b0555a85edfdc402c 100644
--- a/web/modules/webform/js/webform.element.options.admin.js
+++ b/web/modules/webform/js/webform.element.options.admin.js
@@ -15,7 +15,7 @@
    */
   Drupal.behaviors.webformOptionsAdmin = {
     attach: function (context) {
-      $(context).find('.js-form-type-webform-options .js-webform-options-value').once('webform-options-value').each(function() {
+      $(context).find('.js-webform-options-sync').once('js-webform-options-sync').each(function () {
         // Target input name and not id because the id will be changing via
         // Ajax callbacks.
         var name = this.name;
@@ -47,7 +47,7 @@
           }
         });
 
-      })
+      });
     }
   };
 
diff --git a/web/modules/webform/js/webform.element.other.js b/web/modules/webform/js/webform.element.other.js
index 72a2e4b0d8a223fe20aefae785de251e47162715..88f46438a73840461f0bbcb72c3bdf92f7970fb4 100644
--- a/web/modules/webform/js/webform.element.other.js
+++ b/web/modules/webform/js/webform.element.other.js
@@ -33,11 +33,15 @@
 
       // Display the element.
       $element[showEffect]();
-      // Focus and require the input.
-      $input.focus().prop('required', true).attr('aria-required', 'true');
+      // If not initializing, then focus the other element.
+      if (effect !== false) {
+        $input.focus();
+      }
+      // Require the input.
+      $input.prop('required', true).attr('aria-required', 'true');
       // Restore the input's value.
       var value = $input.data('webform-value');
-      if (value !== undefined) {
+      if (typeof value !== 'undefined') {
         $input.val(value);
         var input = $input.get(0);
         // Move cursor to the beginning of the other text input.
diff --git a/web/modules/webform/js/webform.element.radios.js b/web/modules/webform/js/webform.element.radios.js
index c01fe32f8b84c0f8faba5915e813be445138491a..bcf014ee70f12f0c0a4ac929322ba0abf2eee39f 100644
--- a/web/modules/webform/js/webform.element.radios.js
+++ b/web/modules/webform/js/webform.element.radios.js
@@ -16,7 +16,7 @@
    */
   Drupal.behaviors.webformRadiosRequired = {
     attach: function (context) {
-      $('.js-webform-type-radios[required="required"], .js-form-type-webform-radios-other[required="required"]', context).each(function() {
+      $('.js-webform-type-radios.required, .js-webform-type-webform-radios-other.required', context).each(function () {
         $(this).find('input[type="radio"]').attr({'required': 'required', 'aria-required': 'true'});
       });
     }
diff --git a/web/modules/webform/js/webform.element.range.js b/web/modules/webform/js/webform.element.range.js
index 87bdf62d4709fe93755551fe7d9812336219b221..5c1a8d7aa63836623fe19ea3a2aea8c45e1428d6 100644
--- a/web/modules/webform/js/webform.element.range.js
+++ b/web/modules/webform/js/webform.element.range.js
@@ -39,7 +39,7 @@
         });
       });
     }
-  }
+  };
 
   /**
    * Display HTML5 range output in a floating bubble.
@@ -66,7 +66,6 @@
           return;
         }
 
-
         $element.css('position', 'relative');
 
         $input.on('input', function () {
@@ -91,7 +90,7 @@
           // range's buttons so we only incrementally move the output bubble.
           var inputWidth = $input.outerWidth();
           var buttonPosition = Math.floor(inputWidth * (inputValue - $input.attr('min')) / ($input.attr('max') - $input.attr('min')));
-          var increment = Math.floor(inputWidth/5);
+          var increment = Math.floor(inputWidth / 5);
           var outputWidth = $output.outerWidth();
 
           // Set output left position.
@@ -106,7 +105,7 @@
             }
           }
           else if (buttonPosition <= increment * 3) {
-            left = (increment * 2.5) - (outputWidth/2);
+            left = (increment * 2.5) - (outputWidth / 2);
 
           }
           else if (buttonPosition <= increment * 4) {
@@ -122,29 +121,28 @@
           left = Math.floor($input.position().left + left);
 
           // Finally, position the output.
-          $output.css({top: top, left: left})
+          $output.css({top: top, left: left});
         })
-        // Fake a change to position output at page load.
-        .trigger('input');
+          // Fake a change to position output at page load.
+          .trigger('input');
 
         // Add fade in/out event handlers if opacity is defined.
         var defaultOpacity = $output.css('opacity');
         if (defaultOpacity < 1) {
           // Fade in/out on focus/blur of the input.
-          $input.on('focus mouseover', function() {
+          $input.on('focus mouseover', function () {
             $output.stop().fadeTo('slow', 1);
           });
-          $input.on('blur mouseout', function() {
+          $input.on('blur mouseout', function () {
             $output.stop().fadeTo('slow', defaultOpacity);
           });
           // Also fade in when focusing the output.
-          $output.on('touchstart mouseover', function() {
+          $output.on('touchstart mouseover', function () {
             $output.stop().fadeTo('slow', 1);
           });
         }
       });
     }
-  }
-
+  };
 
 })(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.element.rating.js b/web/modules/webform/js/webform.element.rating.js
index 482a07bf6b1408e03a86306bb1743f63b43cae50..627e64604622adb70714a4ba292e18e54d6b345b 100644
--- a/web/modules/webform/js/webform.element.rating.js
+++ b/web/modules/webform/js/webform.element.rating.js
@@ -28,6 +28,11 @@
           var $rateit = $(this);
           var $input = $($rateit.attr('data-rateit-backingfld'));
 
+          // Rateit only initialize inputs on load.
+          if (document.readyState === 'complete') {
+            $rateit.rateit();
+          }
+
           // Update the RateIt widget when the input's value has changed.
           // @see webform.states.js
           $input.on('change', function () {
diff --git a/web/modules/webform/js/webform.element.select2.js b/web/modules/webform/js/webform.element.select2.js
index 39698e2dcc1ffde70e0b48a276e1f65028273e09..fc7a7a382c970904ce49afc014a62e6c55da6fa5 100644
--- a/web/modules/webform/js/webform.element.select2.js
+++ b/web/modules/webform/js/webform.element.select2.js
@@ -28,32 +28,59 @@
         .once('webform-select2')
         // http://stackoverflow.com/questions/14313001/select2-not-calculating-resolved-width-correctly-if-select-is-hidden
         .css('width', '100%')
-        .select2(Drupal.webform.select2.options);
-
-
-      /**
-       * ISSUE:
-       * Hiding/showing element via #states API cause select2 dropdown to appear in the wrong position.
-       *
-       * WORKAROUND:
-       * Close (aka hide) select2 dropdown when #states API hides or shows an element.
-       *
-       * Steps to reproduce:
-       * - Add custom 'Submit button(s)'
-       * - Hide submit button
-       * - Save
-       * - Open 'Submit button(s)' dialog
-       *
-       * Dropdown body is positioned incorrectly when dropdownParent isn't statically positioned.
-       * @see https://github.com/select2/select2/issues/3303
-       */
-      $(function () {
-        $(document).on('state:visible state:visible-slide', function (e) {
-          $('select.select2-hidden-accessible').select2('close');
+        .each(function () {
+          var $select = $(this);
+
+          var options = $.extend({}, Drupal.webform.select2.options);
+          if ($select.data('placeholder')) {
+            options.placeholder = $select.data('placeholder');
+            if (!$select.prop('multiple')) {
+              // Allow single option to be deselected.
+              options.allowClear = true;
+            }
+          }
+
+          $select.select2(options);
         });
-      });
 
     }
   };
 
+  /**
+   * ISSUE:
+   * Hiding/showing element via #states API cause select2 dropdown to appear in the wrong position.
+   *
+   * WORKAROUND:
+   * Close (aka hide) select2 dropdown when #states API hides or shows an element.
+   *
+   * Steps to reproduce:
+   * - Add custom 'Submit button(s)'
+   * - Hide submit button
+   * - Save
+   * - Open 'Submit button(s)' dialog
+   *
+   * Dropdown body is positioned incorrectly when dropdownParent isn't statically positioned.
+   * @see https://github.com/select2/select2/issues/3303
+   */
+  $(function () {
+    if ($.fn.select2) {
+      $(document).on('state:visible state:visible-slide', function (e) {
+        $('select.select2-hidden-accessible').select2('close');
+      });
+    }
+
+    // Select2 search broken inside jQuery UI 1.10.x modal Dialog.
+    // @see https://github.com/select2/select2/issues/1246
+    if ($.ui && $.ui.dialog && $.ui.dialog.prototype._allowInteraction) {
+      var ui_dialog_interaction = $.ui.dialog.prototype._allowInteraction;
+      $.ui.dialog.prototype._allowInteraction = function(e) {
+        if ($(e.target).closest('.select2-dropdown').length) {
+          return true;
+        }
+        return ui_dialog_interaction.apply(this, arguments);
+      };
+    }
+  });
+
+
 })(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.element.signature.js b/web/modules/webform/js/webform.element.signature.js
index f00d71dca76a30ac14e259ba5a148dfe93de7edd..38bf87e80a9090e08d9ea9f9240b7e6fda154f32 100644
--- a/web/modules/webform/js/webform.element.signature.js
+++ b/web/modules/webform/js/webform.element.signature.js
@@ -23,6 +23,7 @@
         return;
       }
 
+
       $(context).find('input.js-webform-signature').once('webform-signature').each(function () {
         var $input = $(this);
         var value = $input.val();
@@ -30,12 +31,17 @@
         var $canvas = $wrapper.find('canvas');
         var $button = $wrapper.find(':button, :submit');
         var canvas = $canvas[0];
+
+        var calculateDimensions = function () {
+          $canvas.attr('width', $wrapper.width());
+          $canvas.attr('height', $wrapper.width() / 3);
+        };
+
         // Set height.
         $canvas.attr('width', $wrapper.width());
         $canvas.attr('height', $wrapper.width() / 3);
         $(window).resize(function () {
-          $canvas.attr('width', $wrapper.width());
-          $canvas.attr('height', $wrapper.width() / 3);
+          calculateDimensions();
 
           // Resizing clears the canvas so we need to reset the signature pad.
           signaturePad.clear();
@@ -61,17 +67,23 @@
         // Set reset handler.
         $button.on('click', function () {
           signaturePad.clear();
-          $input.val();
+          $input.val('');
           this.blur();
           return false;
         });
 
         // Input onchange clears signature pad if value is empty.
+        // Onchange events handlers are triggered when a webform is
+        // hidden or shown.
         // @see webform.states.js
+        // @see triggerEventHandlers()
         $input.on('change', function () {
           if (!$input.val()) {
             signaturePad.clear();
           }
+          setTimeout(function () {
+            calculateDimensions();
+          }, 1);
         });
 
         // Turn signature pad off/on when the input is disabled/enabled.
diff --git a/web/modules/webform/js/webform.element.states.js b/web/modules/webform/js/webform.element.states.js
new file mode 100644
index 0000000000000000000000000000000000000000..1f59bfb5c9c2bf27f389e68b83d3e4a554daa823
--- /dev/null
+++ b/web/modules/webform/js/webform.element.states.js
@@ -0,0 +1,102 @@
+/**
+ * @file
+ * JavaScript behaviors for element #states.
+ */
+
+(function ($, Drupal, drupalSettings) {
+
+  'use strict';
+
+  /**
+   * Element #states builder.
+   *
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.webformElementStates = {
+    attach: function (context) {
+      $(context).find('.webform-states-table--condition').once('webform-element-states-condition').each(function () {
+        var $condition = $(this);
+        var $selector = $condition.find('.webform-states-table--selector select');
+        var $value = $condition.find('.webform-states-table--value input');
+        var $trigger = $condition.find('.webform-states-table--trigger select');
+
+        // Initialize autocompletion.
+        $value.autocomplete({minLength: 0}).on('focus', function () {
+          $value.autocomplete('search', '');
+        });
+
+        // Initialize trigger and selector.
+        $trigger.on('change', function () {$selector.change();});
+
+        $selector.on('change', function () {
+          var selector = $selector.val();
+          var sourceKey = drupalSettings.webformElementStates.selectors[selector];
+          var source = drupalSettings.webformElementStates.sources[sourceKey];
+          var notPattern = ($trigger.val().indexOf('pattern') === -1);
+          if (source && notPattern) {
+            // Enable autocompletion.
+            $value
+              .autocomplete('option', 'source', source)
+              .addClass('form-autocomplete');
+          }
+          else {
+            // Disable autocompletion.
+            $value
+              .autocomplete('option', 'source', [])
+              .removeClass('form-autocomplete');
+          }
+        }).change();
+      });
+
+      // If the states:state is required or optional the required checkbox
+      // should be checked and disabled.
+      var $state = $(context).find('.webform-states-table--state select');
+      if ($state.length) {
+        $state.once('webform-element-states-state')
+          .on('change', toggleRequiredCheckbox);
+        toggleRequiredCheckbox();
+      }
+    }
+  };
+
+  /**
+   * Track required checked state.
+   *
+   * @type {null|boolean}
+   */
+  var requiredChecked = null;
+
+  /**
+   * Toggle the required checkbox when states:state is required or optional.
+   */
+  function toggleRequiredCheckbox() {
+    var $input = $('input[name="properties[required]"]');
+    if (!$input.length) {
+      return;
+    }
+
+    // Determine if any states:state is required or optional.
+    var required = false;
+    $('.webform-states-table--state select').each(function () {
+      var value = $(this).val();
+      if (value === 'required' || value === 'optional') {
+        required = true;
+      }
+    });
+
+    if (required) {
+      requiredChecked = $input.prop('checked');
+      $input.attr('disabled', true);
+      $input.prop('checked', true);
+    }
+    else {
+      $input.attr('disabled', false);
+      if (requiredChecked !== null) {
+        $input.prop('checked', requiredChecked);
+        requiredChecked = null;
+      }
+    }
+    $input.change();
+  }
+
+})(jQuery, Drupal, drupalSettings);
diff --git a/web/modules/webform/js/webform.element.tableselect.js b/web/modules/webform/js/webform.element.tableselect.js
new file mode 100644
index 0000000000000000000000000000000000000000..45751a98cca881be270a8dc975ff33632c5939d8
--- /dev/null
+++ b/web/modules/webform/js/webform.element.tableselect.js
@@ -0,0 +1,66 @@
+/**
+ * @file
+ * JavaScript behaviors for tableselect enhancements.
+ *
+ * @see core/misc/tableselect.es6.js
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * Initialize and tweak webform tableselect behavior.
+   *
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.webformTableSelect = {
+    attach: function (context) {
+      $(context)
+        .find('table.js-webform-tableselect')
+        .once('webform-tableselect')
+        .each(Drupal.webformTableSelect);
+    }
+  };
+
+  /**
+   * Callback used in {@link Drupal.behaviors.tableSelect}.
+   */
+  Drupal.webformTableSelect = function () {
+    var $table = $(this);
+
+    // Set default table rows to .selected class.
+    $table.find('tr').each(function () {
+      // Set table row selected for checkboxes.
+      var $tr = $(this);
+      if ($tr.find('input[type="checkbox"]:checked').length && !$tr.hasClass('selected')) {
+        $tr.addClass('selected');
+      }
+    });
+
+    // Add .selected class event handler to all tableselect elements.
+    // Currently .selected is only added to tables with .select-all.
+    if ($table.find('th.select-all').length === 0) {
+      $table.find('td input[type="checkbox"]:enabled').on('click', function () {
+        $(this).closest('tr').toggleClass('selected', this.checked);
+      });
+    }
+
+    // Add click event handler to the table row that toggles the
+    // checkbox or radio.
+    $table.find('tr').on('click', function (event) {
+      if ($.inArray(event.target.tagName, ['A', 'BUTTON', 'INPUT', 'SELECT']) !== -1) {
+        return true;
+      }
+
+      var $tr = $(this);
+      var $checkbox = $tr.find('td input[type="checkbox"]:enabled, td input[type="radio"]:enabled');
+      if ($checkbox.length === 0) {
+        return true;
+      }
+
+      $checkbox.click();
+    });
+  };
+
+})(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.element.telephone.js b/web/modules/webform/js/webform.element.telephone.js
index c60d2f65c57ee0f2055caffd144b8fe123bd084f..c8e0ecfbd15437ef410245f4f61ae6e7aa7c9d2a 100644
--- a/web/modules/webform/js/webform.element.telephone.js
+++ b/web/modules/webform/js/webform.element.telephone.js
@@ -30,9 +30,11 @@
         var $error = $('<div class="form-item--error-message">' + Drupal.t('Invalid phone number') + '</div>').hide();
         $telephone.closest('.js-form-item').append($error);
 
-        // @todo: Figure out how to lazy load utilsScript (build/js/utils.js).
-        // @see https://github.com/jackocnr/intl-tel-input#utilities-script
         var options = $.extend({
+          // The utilsScript is fetched when the page has finished.
+          // @see \Drupal\webform\Plugin\WebformElement\Telephone::prepare
+          // @see https://github.com/jackocnr/intl-tel-input
+          utilsScript: drupalSettings.webform.intlTelInput.utilsScript,
           nationalMode: false,
           initialCountry: $telephone.attr('data-webform-telephone-international-initial-country') || ''
         }, Drupal.webform.intlTelInput.options);
diff --git a/web/modules/webform/js/webform.element.terms_of_service.js b/web/modules/webform/js/webform.element.terms_of_service.js
index e2f2387d9cbc032162770b8e0c5b0205653a96db..55535c5813aa070720945a189af70ae1bfe17154 100644
--- a/web/modules/webform/js/webform.element.terms_of_service.js
+++ b/web/modules/webform/js/webform.element.terms_of_service.js
@@ -21,10 +21,11 @@
     attach: function (context) {
       $(context).find('.js-form-type-webform-terms-of-service').once('webform-terms-of-service').each(function () {
         var $element = $(this);
-        var type = $element.attr('data-webform-terms-of-service-type');
-
+        var $a = $element.find('label a');
         var $details = $element.find('.webform-terms-of-service-details');
 
+        var type = $element.attr('data-webform-terms-of-service-type');
+
         // Initialize the modal.
         if (type === 'modal') {
           // Move details title to attribute.
@@ -43,15 +44,38 @@
           $details.dialog(options);
         }
 
-        $element.find('label a').click(function (event) {
+        // Add aria-* attributes.
+        if (type !== 'modal') {
+          $a.attr({
+            'aria-expanded': false,
+            'aria-controls': $details.attr('id')
+          });
+        }
+
+        // Set event handlers.
+        $a.click(openDetails)
+          .on('keydown', function (event) {
+            // Space or Return.
+            if (event.which === 32 || event.which === 13) {
+              openDetails(event);
+            }
+          });
+
+        function openDetails(event) {
           if (type === 'modal') {
             $details.dialog('open');
           }
           else {
-            $details.slideToggle();
+            var expanded = ($a.attr('aria-expanded') === 'true');
+
+            // Toggle `aria-expanded` attributes on link.
+            $a.attr('aria-expanded', !expanded);
+
+            // Toggle details.
+            $details[expanded ? 'slideUp' : 'slideDown']();
           }
           event.preventDefault();
-        });
+        }
       });
     }
   };
diff --git a/web/modules/webform/js/webform.element.time.js b/web/modules/webform/js/webform.element.time.js
index 9b987075e254df9cf77594580934e8217a3026b8..2ca0c7b51aff754744346a9159ea04b6c7a5dff6 100644
--- a/web/modules/webform/js/webform.element.time.js
+++ b/web/modules/webform/js/webform.element.time.js
@@ -31,7 +31,7 @@
 
         // Skip if time inputs are supported by the browser and input is not a text field.
         // @see \Drupal\webform\Element\WebformDatetime
-        if (window.Modernizr && Modernizr.inputtypes.time === true && $input.attr('type') !== 'text') {
+        if (window.Modernizr && Modernizr.inputtypes && Modernizr.inputtypes.time === true && $input.attr('type') !== 'text') {
           return;
         }
 
@@ -62,6 +62,6 @@
         $input.timepicker(options);
       });
     }
-  }
+  };
 
 })(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.element.toggle.js b/web/modules/webform/js/webform.element.toggle.js
index d366b30a6903e70cec53530f47f5cf14c24ca41a..e6f146852c1ff7f0f90151aa1a69e831533c4735 100644
--- a/web/modules/webform/js/webform.element.toggle.js
+++ b/web/modules/webform/js/webform.element.toggle.js
@@ -43,10 +43,10 @@
 
         // Trigger change event for #states API.
         // @see Drupal.states.Trigger.states.checked.change
-        $toggle.on('toggle', function() {
-          $checkbox.trigger("change");
+        $toggle.on('toggle', function () {
+          $checkbox.trigger('change');
         });
-        
+
         // If checkbox is disabled then add the .disabled class to the toggle.
         if ($checkbox.attr('disabled') || $checkbox.attr('readonly')) {
           $toggle.addClass('disabled');
@@ -66,7 +66,7 @@
         var $wrapper = $toggle.parent();
         var $checkbox = $wrapper.find('input[type="checkbox"]');
         var isDisabled = ($checkbox.attr('disabled') || $checkbox.attr('readonly'));
-        (isDisabled) ? $toggle.addClass('disabled') : $toggle.removeClass('disabled');
+        $toggle[isDisabled ? 'disabled' : 'disabled']();
       });
     });
   }
diff --git a/web/modules/webform/js/webform.filter.js b/web/modules/webform/js/webform.filter.js
new file mode 100644
index 0000000000000000000000000000000000000000..aee14d184864f31b5cbd65a036d540e6b5c2c5bd
--- /dev/null
+++ b/web/modules/webform/js/webform.filter.js
@@ -0,0 +1,145 @@
+/**
+ * @file
+ * JavaScript behaviors for filter by text.
+ */
+
+(function ($, Drupal, debounce) {
+
+  'use strict';
+
+  /**
+   * Filters the webform element list by a text input search string.
+   *
+   * The text input will have the selector `input.webform-form-filter-text`.
+   *
+   * The target element to do searching in will be in the selector
+   * `input.webform-form-filter-text[data-element]`
+   *
+   * The text source where the text should be found will have the selector
+   * `.webform-form-filter-text-source`
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches the behavior for the webform element filtering.
+   */
+  Drupal.behaviors.webformFilterByText = {
+    attach: function (context, settings) {
+      $('input.webform-form-filter-text', context).once('webform-form-filter-text').each(function () {
+        var $input = $(this);
+        var $table = $($input.data('element'));
+        var $summary = $($input.data('summary'));
+        var $noResults = $($input.data('no-results'));
+        var $details = $table.closest('details');
+        var $filterRows;
+
+        var focusInput = $input.data('focus') || 'true';
+        var sourceSelector = $input.data('source') || '.webform-form-filter-text-source';
+        var parentSelector = $input.data('parent') || 'tr';
+        var selectedSelector = $input.data('selected') || '';
+
+        var hasDetails = $details.length;
+        var totalItems;
+        var args = {
+          '@item': $input.data('item-singlular') || Drupal.t('item'),
+          '@items': $input.data('item-plural') || Drupal.t('items'),
+          '@total': null
+        };
+
+        if ($table.length) {
+          $filterRows = $table.find(sourceSelector);
+          $input
+            .attr('autocomplete', 'off')
+            .on('keyup', debounce(filterElementList, 200))
+            .keyup();
+
+          // Make sure the filter input is always focused.
+          if (focusInput === 'true') {
+            setTimeout(function () {$input.focus();});
+          }
+        }
+
+        /**
+         * Filters the webform element list.
+         *
+         * @param {jQuery.Event} e
+         *   The jQuery event for the keyup event that triggered the filter.
+         */
+        function filterElementList(e) {
+          var query = $(e.target).val().toLowerCase();
+
+          // Filter if the length of the query is at least 2 characters.
+          if (query.length >= 2) {
+            // Reset count.
+            totalItems = 0;
+            if ($details.length) {
+              $details.hide();
+            }
+            $filterRows.each(toggleEntry);
+
+            // Announce filter changes.
+            // @see Drupal.behaviors.blockFilterByText
+            Drupal.announce(Drupal.formatPlural(
+              totalItems,
+              '1 @item is available in the modified list.',
+              '@total @items are available in the modified list.',
+              args
+            ));
+          }
+          else {
+            totalItems = $filterRows.length;
+            $filterRows.each(function (index) {
+              $(this).closest(parentSelector).show();
+              if ($details.length) {
+                $details.show();
+              }
+            });
+          }
+
+          // Set total.
+          args['@total'] = totalItems;
+
+          // Hide/show no results.
+          $noResults[totalItems ? 'hide' : 'show']();
+
+          // Update summary.
+          if ($summary.length) {
+            $summary.html(Drupal.formatPlural(
+              totalItems,
+              '1 @item',
+              '@total @items',
+              args
+            ));
+            $summary[totalItems ? 'show' : 'hide']();
+          }
+
+          /**
+           * Shows or hides the webform element entry based on the query.
+           *
+           * @param {number} index
+           *   The index in the loop, as provided by `jQuery.each`
+           * @param {HTMLElement} label
+           *   The label of the webform.
+           */
+          function toggleEntry(index, label) {
+            var $label = $(label);
+            var $row = $label.closest(parentSelector);
+
+            var textMatch = $label.text().toLowerCase().indexOf(query) !== -1;
+            var isSelected = (selectedSelector && $row.find(selectedSelector).length) ? true : false;
+
+            var isVisible = textMatch || isSelected;
+            $row.toggle(isVisible);
+            if (isVisible) {
+              totalItems++;
+              if (hasDetails) {
+                $row.closest('details').show();
+              }
+            }
+          }
+        }
+      });
+    }
+  };
+
+})(jQuery, Drupal, Drupal.debounce);
diff --git a/web/modules/webform/js/webform.form.js b/web/modules/webform/js/webform.form.js
index 5ac9a6788f6d95d462a2d792bf77814007a60254..3b1f182a033a16d42234ac2efecdaadf5610fde5 100644
--- a/web/modules/webform/js/webform.form.js
+++ b/web/modules/webform/js/webform.form.js
@@ -7,6 +7,28 @@
 
   'use strict';
 
+  /**
+   * Remove single submit event listener.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches the behavior for removing single submit event listener.
+   *
+   * @see Drupal.behaviors.formSingleSubmit
+   */
+  Drupal.behaviors.webformRemoveFormSingleSubmit = {
+    attach: function attach() {
+      function onFormSubmit(e) {
+        var $form = $(e.currentTarget);
+        $form.removeAttr('data-drupal-form-submit-last');
+      }
+      $('body')
+        .once('webform-single-submit')
+        .on('submit.singleSubmit', 'form.webform-remove-single-submit', onFormSubmit);
+    }
+  };
+
   /**
    * Autofocus first input.
    *
@@ -37,8 +59,7 @@
         .not(':button, :submit, :reset, :image, :file')
         .once('webform-disable-autosubmit')
         .on('keyup keypress', function (e) {
-          var keyCode = e.keyCode || e.which;
-          if (keyCode === 13) {
+          if (e.which === 13) {
             e.preventDefault();
             return false;
           }
@@ -53,12 +74,15 @@
    *
    * @prop {Drupal~behaviorAttach} attach
    *   Attaches the behavior for the skipping client-side validation.
+   *
+   * @deprecated in Webform 8.x-5.x and will be removed in Webform 8.x-6.x.
+   *   Use 'formnovalidate' attribute instead.
    */
   Drupal.behaviors.webformSubmitNoValidate = {
     attach: function (context) {
-      $(context).find(':submit.js-webform-novalidate').once('webform-novalidate').on('click', function () {
-        $(this.form).attr('novalidate', 'novalidate');
-      });
+      $(context).find(':submit.js-webform-novalidate')
+        .once('webform-novalidate')
+        .attr('formnovalidate', 'formnovalidate');
     }
   };
 
@@ -80,30 +104,38 @@
   };
 
   /**
-   * Custom required error message.
+   * Custom required and pattern validation error messages.
    *
    * @type {Drupal~behavior}
    *
    * @prop {Drupal~behaviorAttach} attach
-   *   Attaches the behavior for the webform custom required error message.
+   *   Attaches the behavior for the webform custom required and pattern
+   *   validation error messages.
    *
    * @see http://stackoverflow.com/questions/5272433/html5-form-required-attribute-set-custom-validation-message
-   */
+   **/
   Drupal.behaviors.webformRequiredError = {
     attach: function (context) {
-      $(context).find(':input[data-webform-required-error]').once('webform-required-error')
-        .on('invalid', function() {
+      $(context).find(':input[data-webform-required-error], :input[data-webform-pattern-error]').once('webform-required-error')
+        .on('invalid', function () {
           this.setCustomValidity('');
-          if (!this.valid) {
+          if (this.valid) {
+            return;
+          }
+
+          if (this.validity.patternMismatch && $(this).attr('data-webform-pattern-error')) {
+            this.setCustomValidity($(this).attr('data-webform-pattern-error'));
+          }
+          else if (this.validity.valueMissing && $(this).attr('data-webform-required-error')) {
             this.setCustomValidity($(this).attr('data-webform-required-error'));
           }
         })
-        .on('input, change', function() {
+        .on('input, change', function () {
           // Find all related elements by name and reset custom validity.
           // This specifically applies to required radios and checkboxes.
           var name = $(this).attr('name');
-          $(this.form).find(':input[name="' + name + '"]').each(
-            function() {this.setCustomValidity('');
+          $(this.form).find(':input[name="' + name + '"]').each(function () {
+            this.setCustomValidity('');
           });
         });
     }
@@ -113,86 +145,9 @@
   // custom validity.
   $(document).on('state:required', function (e) {
     $(e.target).filter('[data-webform-required-error]')
-      .each(function() {this.setCustomValidity('');});
+      .each(function () {this.setCustomValidity('');});
   });
 
-  /**
-   * Filters the webform element list by a text input search string.
-   *
-   * The text input will have the selector `input.webform-form-filter-text`.
-   *
-   * The target element to do searching in will be in the selector
-   * `input.webform-form-filter-text[data-element]`
-   *
-   * The text source where the text should be found will have the selector
-   * `.webform-form-filter-text-source`
-   *
-   * @type {Drupal~behavior}
-   *
-   * @prop {Drupal~behaviorAttach} attach
-   *   Attaches the behavior for the webform element filtering.
-   */
-  Drupal.behaviors.webformFilterByText = {
-    attach: function (context, settings) {
-      var $input = $('input.webform-form-filter-text').once('webform-form-filter-text');
-      var $table = $($input.attr('data-element'));
-      var $details = $table.closest('details');
-      var $filter_rows;
-
-      /**
-       * Filters the webform element list.
-       *
-       * @param {jQuery.Event} e
-       *   The jQuery event for the keyup event that triggered the filter.
-       */
-      function filterElementList(e) {
-        var query = $(e.target).val().toLowerCase();
-
-        /**
-         * Shows or hides the webform element entry based on the query.
-         *
-         * @param {number} index
-         *   The index in the loop, as provided by `jQuery.each`
-         * @param {HTMLElement} label
-         *   The label of the webform.
-         */
-        function toggleEntry(index, label) {
-          var $label = $(label);
-          var $row = $label.closest('tr');
-          var textMatch = $label.text().toLowerCase().indexOf(query) !== -1;
-          $row.toggle(textMatch);
-          if (textMatch && $details.length) {
-            $row.closest('details').show();
-          }
-        }
-
-        // Filter if the length of the query is at least 2 characters.
-        if (query.length >= 2) {
-          if ($details.length) {
-            $details.hide();
-          }
-          $filter_rows.each(toggleEntry);
-        }
-        else {
-          $filter_rows.each(function (index) {
-            $(this).closest('tr').show();
-            if ($details.length) {
-              $details.show();
-            }
-          });
-        }
-      }
-
-      if ($table.length) {
-        $filter_rows = $table.find('.webform-form-filter-text-source');
-        $input.on('keyup', filterElementList);
-        if ($input.val()) {
-          $input.keyup();
-        }
-      }
-    }
-  };
-
   if (window.imceInput) {
     window.imceInput.processUrlInput = function (i, el) {
       var button = imceInput.createUrlButton(el.id, el.getAttribute('data-imce-type'));
diff --git a/web/modules/webform/js/webform.form.submit_back.js b/web/modules/webform/js/webform.form.submit_back.js
index 4d024af2f1a2b6ca21886923b6baa9970847b406..642c43b4993d4100896b3d68f4662eae8732b925 100644
--- a/web/modules/webform/js/webform.form.submit_back.js
+++ b/web/modules/webform/js/webform.form.submit_back.js
@@ -11,7 +11,7 @@
   if (window.history && window.history.pushState) {
     window.history.pushState('', null, '');
     window.onpopstate = function (event) {
-     $('#edit-wizard-prev, #edit-preview-prev').click();
+      $('#edit-wizard-prev, #edit-preview-prev').click();
     };
   }
 
diff --git a/web/modules/webform/js/webform.form.submit_once.js b/web/modules/webform/js/webform.form.submit_once.js
index bc2dc2403cc0f04917084b3be3e0b101d641d4d6..ab8414b9d22bdf747f48842a631800d294e87879 100644
--- a/web/modules/webform/js/webform.form.submit_once.js
+++ b/web/modules/webform/js/webform.form.submit_once.js
@@ -19,14 +19,18 @@
     attach: function (context) {
       $('.js-webform-submit-once', context).each(function () {
         var $form = $(this);
-        $form.removeAttr('webform-submitted');
-        $form.find('.form-actions :submit').removeAttr('webform-clicked');
+        // Remove data-webform-submitted.
+        $form.removeData('webform-submitted');
+        // Remove .js-webform-submit-clicked.
+        $form.find('.form-actions :submit').removeClass('js-webform-submit-clicked');
 
         // Track which submit button was clicked.
         // @see http://stackoverflow.com/questions/5721724/jquery-how-to-get-which-button-was-clicked-upon-form-submission
         $form.find('.form-actions :submit').click(function () {
-          $form.find('.form-actions :submit').removeAttr('webform-clicked');
-          $(this).attr('webform-clicked', 'true');
+          $form.find('.form-actions :submit')
+            .removeClass('js-webform-submit-clicked');
+          $(this)
+            .addClass('js-webform-submit-clicked');
         });
 
         $(this).submit(function () {
@@ -36,10 +40,10 @@
           }
 
           // Track webform submitted.
-          if ($form.attr('webform-submitted')) {
+          if ($form.data('webform-submitted')) {
             return false;
           }
-          $form.attr('webform-submitted', 'true');
+          $form.data('webform-submitted', 'true');
 
           // Visually disable all submit buttons.
           // Submit buttons can't disabled because their op(eration) must to be posted back to the server.
@@ -47,7 +51,7 @@
 
           // Set the throbber progress indicator.
           // @see Drupal.Ajax.prototype.setProgressIndicatorThrobber
-          var $clickedButton = $form.find('.form-actions input[type=submit][webform-clicked=true]');
+          var $clickedButton = $form.find('.form-actions :submit.js-webform-submit-clicked');
           var $progress = $('<div class="ajax-progress ajax-progress-throbber"><div class="throbber">&nbsp;</div></div>');
           $clickedButton.after($progress);
         });
diff --git a/web/modules/webform/js/webform.form.tabs.js b/web/modules/webform/js/webform.form.tabs.js
index 63b34fd20da867d31c1c51c22dfb441ed329b763..d8ffaaa0dfaf222b529cdfd8b3df888db706caff 100644
--- a/web/modules/webform/js/webform.form.tabs.js
+++ b/web/modules/webform/js/webform.form.tabs.js
@@ -27,27 +27,21 @@
    */
   Drupal.behaviors.webformFormTabs = {
     attach: function (context) {
-      // Set active tab and clear the location hash once it is set.
-      if (location.hash) {
-        var active = $('a[href="' + location.hash + '"]').data('tab-index');
-        if (active !== undefined) {
-          Drupal.webform.formTabs.options.active = active;
-          location.hash = '';
-        }
-      }
-
-      $(context).find('div.webform-tabs').once('webform-tabs').each(function() {
+      $(context).find('div.webform-tabs').once('webform-tabs').each(function () {
         var $tabs = $(this);
         var options = jQuery.extend({}, Drupal.webform.formTabs.options);
 
-        // Set active tab from data-tab-active attribute.
-        var tab_name = $tabs.attr('data-tab-active');
-        if (tab_name) {
-          options.active = $('a[href="#' + tab_name + '"]').data('tab-index');
+        // Set active tab and clear the location hash once it is set.
+        if (location.hash) {
+          var active = $('a[href="' + location.hash + '"]').data('tab-index');
+          if (typeof active !== 'undefined') {
+            options.active = active;
+            location.hash = '';
+          }
         }
 
         $tabs.tabs(options);
-      })
+      });
     }
   };
 
diff --git a/web/modules/webform/js/webform.form.unsaved.js b/web/modules/webform/js/webform.form.unsaved.js
index c6e2c03003ffcc5d23313636644eea89f2f7b68c..25498478641048db46f4070ac25a768e9d929c35 100644
--- a/web/modules/webform/js/webform.form.unsaved.js
+++ b/web/modules/webform/js/webform.form.unsaved.js
@@ -18,11 +18,16 @@
    *   Attaches the behavior for unsaved changes.
    */
   Drupal.behaviors.webformUnsaved = {
+    clear: function () {
+      // Allow Ajax refresh/redirect to clear unsaved flag.
+      // @see Drupal.AjaxCommands.prototype.webformRefresh
+      unsaved = false;
+    },
     attach: function (context) {
-      // Look for the 'data-webform-unsaved' attribute which indicates that the
-      // multi-step webform has unsaved data.
+      // Look for the 'data-webform-unsaved' attribute which indicates that
+      // a multi-step webform has unsaved data.
       // @see \Drupal\webform\WebformSubmissionForm::buildForm
-      if ($('.js-webform-unsaved[data-webform-unsaved]').length) {
+      if ($('.js-webform-unsaved[data-webform-unsaved]').once('data-webform-unsaved').length) {
         unsaved = true;
       }
       else {
@@ -36,9 +41,38 @@
         });
       }
 
-      $('.js-webform-unsaved button, .js-webform-unsaved input[type="submit"]', context).once('webform-unsaved').on('click', function () {
+      $('.js-webform-unsaved button, .js-webform-unsaved input[type="submit"]', context).once('webform-unsaved').on('click', function (event) {
+        // For reset button we must confirm unsaved changes before the
+        // before unload event handler.
+        if ($(this).hasClass('webform-button--reset') && unsaved) {
+          if (!window.confirm(Drupal.t('Changes you made may not be saved.') + '\n\n' + Drupal.t('Press OK to leave this page or Cancel to stay.'))) {
+            return false;
+          }
+        }
+
         unsaved = false;
       });
+
+      // Add submit handler to form.beforeSend.
+      // Update Drupal.Ajax.prototype.beforeSend only once.
+      if (typeof Drupal.Ajax !== 'undefined' && typeof Drupal.Ajax.prototype.beforeSubmitWebformUnsavedOriginal === 'undefined') {
+        Drupal.Ajax.prototype.beforeSubmitWebformUnsavedOriginal = Drupal.Ajax.prototype.beforeSubmit;
+        Drupal.Ajax.prototype.beforeSubmit = function (form_values, element_settings, options) {
+          unsaved = false;
+          return this.beforeSubmitWebformUnsavedOriginal();
+        };
+      }
+
+      // Track all CKEditor change events.
+      // @see https://ckeditor.com/old/forums/Support/CKEditor-jQuery-change-event
+      if (window.CKEDITOR && !CKEDITOR.webformUnsaved) {
+        CKEDITOR.webformUnsaved = true;
+        CKEDITOR.on('instanceCreated', function (event) {
+          event.editor.on('change', function (evt) {
+            unsaved = true;
+          });
+        });
+      }
     }
   };
 
@@ -65,9 +99,9 @@
     }
     $('a').bind('click', function (evt) {
       var href = $(evt.target).closest('a').attr('href');
-      if (href !== undefined && !(href.match(/^#/) || href.trim() === '')) {
+      if (typeof href !== 'undefined' && !(href.match(/^#/) || href.trim() === '')) {
         if ($(window).triggerHandler('beforeunload')) {
-          if (!confirm(Drupal.t('Changes you made may not be saved.') + '\n\n' + Drupal.t('Press OK to leave this page or Cancel to stay.'))) {
+          if (!window.confirm(Drupal.t('Changes you made may not be saved.') + '\n\n' + Drupal.t('Press OK to leave this page or Cancel to stay.'))) {
             return false;
           }
         }
diff --git a/web/modules/webform/js/webform.form.wizard.js b/web/modules/webform/js/webform.form.wizard.js
deleted file mode 100644
index 5d310978cbe057f06ce93ee9f7a5a91e6bcdc0c6..0000000000000000000000000000000000000000
--- a/web/modules/webform/js/webform.form.wizard.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * @file
- * JavaScript behaviors for webform wizard.
- */
-
-(function ($, Drupal) {
-
-  'use strict';
-
-  /**
-   * Tracks the wizard's current page in the URL.
-   *
-   * @type {Drupal~behavior}
-   *
-   * @prop {Drupal~behaviorAttach} attach
-   *   Tracks the wizard's current page in the URL.
-   */
-  Drupal.behaviors.webformWizardTrackPage = {
-    attach: function (context) {
-      // Make sure on page load or Ajax refresh the location ?page is correct
-      // since conditional logic can skip pages.
-      // Note: window.history is only supported by IE 10+ and all other browsers.
-      if (window.history && history.replaceState) {
-        $('form[data-webform-wizard-current-page]', context).once('webform-wizard-current-page').each(function () {
-          var page = $(this).attr('data-webform-wizard-current-page');
-          history.replaceState(null, null, window.location.toString().replace(/\?.+$/, '') + '?page=' + page);
-        });
-      }
-
-      // When paging next and back update the URL so that Drupal knows what
-      // the expected page name or index is going to be.
-      // NOTE: If conditional wizard page logic is configured the
-      // expected page name or index may not be accurate.
-      $(':button[data-webform-wizard-page], :submit[data-webform-wizard-page]', context).once('webform-wizard-page').on('click', function() {
-        var page = $(this).attr('data-webform-wizard-page');
-        this.form.action = this.form.action.replace(/\?.+$/, '') + '?page=' + page;
-      });
-    }
-  };
-
-
-})(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.imce.js b/web/modules/webform/js/webform.imce.js
index e6d3f383f9fb6733a590247dd1a67ac7c1876b26..7f59b38fa636fcc02917debffd47362b4310203b 100644
--- a/web/modules/webform/js/webform.imce.js
+++ b/web/modules/webform/js/webform.imce.js
@@ -9,6 +9,11 @@
 
   /**
    * Override processUrlInput to place the 'Open File Browser' links after the target element.
+   *
+   * @param {int} i
+   *   Element's index.
+   * @param {element} el
+   *   A element.
    */
   window.imceInput.processUrlInput = function (i, el) {
     var button = imceInput.createUrlButton(el.id, el.getAttribute('data-imce-type'));
diff --git a/web/modules/webform/js/webform.jquery.ui.dialog.js b/web/modules/webform/js/webform.jquery.ui.dialog.js
new file mode 100644
index 0000000000000000000000000000000000000000..479fc2be0ffaf9d2c9f1acb1a2565d0e61ece533
--- /dev/null
+++ b/web/modules/webform/js/webform.jquery.ui.dialog.js
@@ -0,0 +1,52 @@
+/**
+ * @file
+ * JavaScript behaviors to fix jQuery UI dialogs.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * Ensure that ckeditor has focus when displayed inside of jquery-ui dialog widget
+   *
+   * @see http://stackoverflow.com/questions/20533487/how-to-ensure-that-ckeditor-has-focus-when-displayed-inside-of-jquery-ui-dialog
+   */
+  var _allowInteraction = $.ui.dialog.prototype._allowInteraction;
+  $.ui.dialog.prototype._allowInteraction = function (event) {
+    if ($(event.target).closest('.cke_dialog').length) {
+      return true;
+    }
+    return _allowInteraction.apply(this, arguments);
+  };
+
+  /**
+   * Attaches webform dialog behaviors.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches event listeners for webform dialogs.
+   */
+  Drupal.behaviors.webformDialogEvents = {
+    attach: function () {
+      $(window).once('webform-dialog').on({
+        'dialog:aftercreate': function (event, dialog, $element, settings) {
+          setTimeout(function () {
+            var hasFocus = $element.find('[autofocus]:tabbable');
+            if (!hasFocus.length) {
+              // Move focus to first input which is not a button.
+              hasFocus = $element.find(':input:tabbable:not(:button)');
+            }
+            if (!hasFocus.length) {
+              // Move focus to close dialog button.
+              hasFocus = $element.parent().find('.ui-dialog-titlebar-close');
+            }
+            hasFocus.eq(0).focus();
+          });
+        }
+      });
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.off-canvas.js b/web/modules/webform/js/webform.off-canvas.js
new file mode 100644
index 0000000000000000000000000000000000000000..1ce8a282788bf8f5bada2bbeee0a895bae512b8c
--- /dev/null
+++ b/web/modules/webform/js/webform.off-canvas.js
@@ -0,0 +1,39 @@
+/**
+ * @file
+ * JavaScript behaviors for webform off-canvas dialogs.
+ *
+ * @see misc/dialog/off-canvas.js
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * Attaches webform off-canvas behaviors.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches event listeners to window for off-canvas dialogs.
+   */
+  Drupal.behaviors.webformOffCanvasEvents = {
+    attach: function () {
+      // Resize seven.theme tabs when off-canvas dialog opened and closed.
+      // @see core/themes/seven/js/nav-tabs.js
+      $(window).once('webform-off-canvas').on({
+        'dialog:aftercreate': function (event, dialog, $element, settings) {
+          if (Drupal.offCanvas.isOffCanvas($element)) {
+            $(window).trigger('resize.tabs');
+          }
+        },
+        'dialog:afterclose': function (event, dialog, $element, settings) {
+          if (Drupal.offCanvas.isOffCanvas($element)) {
+            $(window).trigger('resize.tabs');
+          }
+        }
+      });
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.states.js b/web/modules/webform/js/webform.states.js
index 3daae913bdae72af26c0fe662f3af186c080be03..1e1ee9c543dfdda6a34780489f4e0cceb893b4ec 100644
--- a/web/modules/webform/js/webform.states.js
+++ b/web/modules/webform/js/webform.states.js
@@ -20,7 +20,7 @@
    * @param {string} data
    *   The data attribute name.
    *
-   * @returns {boolean}
+   * @return {boolean}
    *   TRUE if an element has a specified data attribute.
    */
   $.fn.hasData = function (data) {
@@ -30,7 +30,7 @@
   /**
    * Check if element is within the webform or not.
    *
-   * @returns {boolean}
+   * @return {boolean}
    *   TRUE if element is within the webform.
    */
   $.fn.isWebform = function () {
@@ -45,20 +45,56 @@
     return this.val() === '';
   };
 
+  // Apply solution included in #1962800 patch.
+  // Issue #1962800: Form #states not working with literal integers as
+  // values in IE11.
+  // @see https://www.drupal.org/project/drupal/issues/1962800
+  // @see https://www.drupal.org/files/issues/core-states-not-working-with-integers-ie11_1962800_46.patch
+  //
+  // This issue causes pattern, less than, and greater than support to break.
+  // @see https://www.drupal.org/project/webform/issues/2981724
+  var states = Drupal.states;
+  Drupal.states.Dependent.prototype.compare = function compare(reference, selector, state) {
+    var value = this.values[selector][state.name];
+
+    var name = reference.constructor.name;
+    if (!name) {
+      name = $.type(reference);
+
+      name = name.charAt(0).toUpperCase() + name.slice(1);
+    }
+    if (name in states.Dependent.comparisons) {
+      return states.Dependent.comparisons[name](reference, value);
+    }
+
+    if (reference.constructor.name in states.Dependent.comparisons) {
+      return states.Dependent.comparisons[reference.constructor.name](reference, value);
+    }
+
+    return _compare2(reference, value);
+  };
+  function _compare2(a, b) {
+    if (a === b) {
+      return typeof a === 'undefined' ? a : true;
+    }
+
+    return typeof a === 'undefined' || typeof b === 'undefined';
+  }
+
   // Adds pattern, less than, and greater than support to #state API.
   // @see http://drupalsun.com/julia-evans/2012/03/09/extending-form-api-states-regular-expressions
   Drupal.states.Dependent.comparisons.Object = function (reference, value) {
     if ('pattern' in reference) {
-      return (new RegExp(reference.pattern)).test(value);
+      return (new RegExp(reference['pattern'])).test(value);
     }
     else if ('!pattern' in reference) {
       return !((new RegExp(reference['!pattern'])).test(value));
     }
     else if ('less' in reference) {
-      return (value !== '' && reference.less > value);
+      return (value !== '' && parseFloat(reference['less']) > parseFloat(value));
     }
     else if ('greater' in reference) {
-      return (value !== '' && reference.greater < value);
+      return (value !== '' && parseFloat(reference['greater']) < parseFloat(value));
     }
     else {
       return reference.indexOf(value) !== false;
@@ -83,12 +119,14 @@
       // @see Issue #2938414: Checkboxes don't support #states required
       // @see Issue #2731991: Setting required on radios marks all options required.
       // @see Issue #2856315: Conditional Logic - Requiring Radios in a Fieldset.
-      if ($target.is('.js-webform-type-radios, .js-webform-type-checkboxes')) {
+      // Fix #required for fieldsets.
+      // @see Issue #2977569: Hidden fieldsets that become visible with conditional logic cannot be made required.
+      if ($target.is('.js-webform-type-radios, .js-webform-type-checkboxes, fieldset')) {
         if (e.value) {
-          $target.find('legend span').addClass('js-form-required form-required');
+          $target.find('legend span.fieldset-legend:not(.visually-hidden)').addClass('js-form-required form-required');
         }
         else {
-          $target.find('legend span').removeClass('js-form-required form-required');
+          $target.find('legend span.fieldset-legend:not(.visually-hidden)').removeClass('js-form-required form-required');
         }
       }
 
@@ -124,6 +162,11 @@
             .unbind('click', checkboxRequiredhandler);
         }
       }
+
+      // Issue #2986017: Fieldsets shouldn't have required attribute.
+      if ($target.is('fieldset')) {
+        $target.removeAttr('required aria-required');
+      }
     }
 
   });
@@ -153,11 +196,11 @@
     }
   });
 
-  $document.bind('state:visible-slide', function(e) {
+  $document.bind('state:visible-slide', function (e) {
     if (e.trigger && $(e.target).isWebform()) {
       var effect = e.value ? 'slideDown' : 'slideUp';
       var duration = Drupal.webform.states[effect].duration;
-      $(e.target).closest('.js-form-item, .js-form-submit, .js-form-wrapper')[effect ](duration);
+      $(e.target).closest('.js-form-item, .js-form-submit, .js-form-wrapper')[effect](duration);
     }
   });
   Drupal.states.State.aliases['invisible-slide'] = '!visible-slide';
@@ -169,11 +212,11 @@
       $(e.target)
         .prop('disabled', e.value)
         .closest('.js-form-item, .js-form-submit, .js-form-wrapper').toggleClass('form-disabled', e.value)
-        .find('select, input, textarea').prop('disabled', e.value);
+        .find('select, input, textarea, button').prop('disabled', e.value);
 
       // Trigger webform:disabled.
       $(e.target).trigger('webform:disabled')
-        .find('select, input, textarea').trigger('webform:disabled');
+        .find('select, input, textarea, button').trigger('webform:disabled');
     }
   });
 
@@ -195,7 +238,7 @@
   /**
    * Trigger an input's event handlers.
    *
-   * @param input
+   * @param {element} input
    *   An input.
    */
   function triggerEventHandlers(input) {
@@ -215,7 +258,7 @@
         .trigger('change', extraParameters)
         .trigger('blur', extraParameters);
     }
-    else if (type !== 'submit' && type !== 'button') {
+    else if (type !== 'submit' && type !== 'button' && type !== 'file') {
       $input
         .trigger('input', extraParameters)
         .trigger('change', extraParameters)
@@ -228,7 +271,7 @@
   /**
    * Backup an input's current value and required attribute
    *
-   * @param input
+   * @param {element} input
    *   An input.
    */
   function backupValueAndRequired(input) {
@@ -262,7 +305,7 @@
   /**
    * Restore an input's value and required attribute.
    *
-   * @param input
+   * @param {element} input
    *   An input.
    */
   function restoreValueAndRequired(input) {
@@ -301,7 +344,7 @@
   /**
    * Clear an input's value and required attributes.
    *
-   * @param input
+   * @param {element} input
    *   An input.
    */
   function clearValueAndRequired(input) {
diff --git a/web/modules/webform/js/webform.tooltip.js b/web/modules/webform/js/webform.tooltip.js
index 545821a239d610d919c9bf3275cb1af602731996..15ff5346dc638db567ceee0362cc165b62805954 100644
--- a/web/modules/webform/js/webform.tooltip.js
+++ b/web/modules/webform/js/webform.tooltip.js
@@ -14,16 +14,16 @@
 
   var tooltipDefaultOptions = {
     // @see https://stackoverflow.com/questions/18231315/jquery-ui-tooltip-html-with-links
-    show: null,
+    show: {delay: 100},
     close: function (event, ui) {
       ui.tooltip.hover(
         function () {
           $(this).stop(true).fadeTo(400, 1);
         },
         function () {
-          $(this).fadeOut("400", function () {
+          $(this).fadeOut('400', function () {
             $(this).remove();
-          })
+          });
         });
     }
   };
@@ -47,14 +47,14 @@
       $(context).find('.js-webform-tooltip-element').once('webform-tooltip-element').each(function () {
         var $element = $(this);
 
-        // Checkboxes, radios, buttons, toggles, etc... use fieldsets.
+        // Checkboxes, radios, buttons, toggles, etc… use fieldsets.
         // @see \Drupal\webform\Plugin\WebformElement\OptionsBase::prepare
         var $description;
         if ($element.is('fieldset')) {
-          $description = $element.find('> .fieldset-wrapper > .field-suffix .description.visually-hidden');
+          $description = $element.find('> .fieldset-wrapper > .description > .webform-element-description.visually-hidden');
         }
         else {
-          $description = $element.children('.description.visually-hidden');
+          $description = $element.find('> .description > .webform-element-description.visually-hidden');
         }
 
         var has_visible_input = $element.find(':input:not([type=hidden])').length;
diff --git a/web/modules/webform/js/webform.wizard.pages.js b/web/modules/webform/js/webform.wizard.pages.js
new file mode 100644
index 0000000000000000000000000000000000000000..805033ad1cbc5578cf385e0fb4c36c70c1bcdbfc
--- /dev/null
+++ b/web/modules/webform/js/webform.wizard.pages.js
@@ -0,0 +1,61 @@
+/**
+ * @file
+ * JavaScript behaviors for webform wizard.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * Link the wizard's previous pages.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Links the wizard's previous pages.
+   */
+  Drupal.behaviors.webformWizardPagesLink = {
+    attach: function (context) {
+      $('.js-webform-wizard-pages-links', context).once('webform-wizard-pages-links').each(function () {
+        var $pages = $(this);
+        var $form = $pages.closest('form');
+
+        var hasProgressLink = $pages.data('wizard-progress-link');
+        var hasPreviewLink = $pages.data('wizard-preview-link');
+
+        $pages.find('.js-webform-wizard-pages-link').each(function () {
+          var $button = $(this);
+          var title = $button.attr('title');
+          var page = $button.data('webform-page');
+
+          // Link progress marker and title.
+          if (hasProgressLink) {
+            var $progress = $form.find('.webform-progress [data-webform-page="' + page + '"]');
+            $progress.find('.progress-marker, .progress-title')
+              .attr({
+                'role': 'link',
+                'title': title,
+                'aria-label': title
+              })
+              .click(function () {
+                $button.click();
+              });
+            // Only allow the marker to be tabbable.
+            $progress.find('.progress-marker').attr('tabindex', 0);
+          }
+
+          // Move button to preview page div container with [data-webform-page].
+          // @see \Drupal\webform\Plugin\WebformElement\WebformWizardPage::formatHtmlItem
+          if (hasPreviewLink) {
+            $form
+              .find('.webform-preview [data-webform-page="' + page + '"]')
+              .append($button)
+              .show();
+          }
+        });
+      });
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/web/modules/webform/js/webform.wizard.track.js b/web/modules/webform/js/webform.wizard.track.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa3974cdcf569f1e531d92940c2621fe52d5cb70
--- /dev/null
+++ b/web/modules/webform/js/webform.wizard.track.js
@@ -0,0 +1,67 @@
+/**
+ * @file
+ * JavaScript behaviors for webform wizard.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * Tracks the wizard's current page in the URL.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Tracks the wizard's current page in the URL.
+   */
+  Drupal.behaviors.webformWizardTrackPage = {
+    attach: function (context) {
+      // Make sure on page load or Ajax refresh the location ?page is correct
+      // since conditional logic can skip pages.
+      // Note: window.history is only supported by IE 10+.
+      if (window.history && window.history.replaceState) {
+        // Track the form's current page for 8.5.x and below.
+        // @todo Remove the below code once only 8.6.x is supported.
+        // @see https://www.drupal.org/project/drupal/issues/2508796
+        $('form[data-webform-wizard-current-page]', context)
+          .once('webform-wizard-current-page')
+          .each(function () {
+            trackPage(this);
+          });
+
+        // Track the form's current page for 8.6.x and above.
+        if ($(context).hasData('webform-wizard-current-page')) {
+          trackPage(context);
+        }
+      }
+
+      // When paging next and back update the URL so that Drupal knows what
+      // the expected page name or index is going to be.
+      // NOTE: If conditional wizard page logic is configured the
+      // expected page name or index may not be accurate.
+      $(':button[data-webform-wizard-page], :submit[data-webform-wizard-page]', context).once('webform-wizard-page').on('click', function () {
+        var page = $(this).attr('data-webform-wizard-page');
+        this.form.action = this.form.action.replace(/\?.+$/, '') + '?page=' + page;
+      });
+
+      /**
+       * Append the form's current page data attribute to the browser's URL.
+       *
+       * @param {HTMLElement} form
+       *   The form element.
+       */
+      function trackPage(form) {
+        var $form = $(form);
+        // Make sure the form is visible before updating the URL.
+        if ($form.is(':visible')) {
+          var page = $form.attr('data-webform-wizard-current-page');
+          var url = window.location.toString().replace(/\?.+$/, '') +
+            '?page=' + page;
+          window.history.replaceState(null, null, url);
+        }
+      }
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/web/modules/webform/modules/webform_access/config/schema/webform_access.entity.webform_access_type.schema.yml b/web/modules/webform/modules/webform_access/config/schema/webform_access.entity.webform_access_type.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dcd532aa76982a7dca7263014478f77f54439218
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/config/schema/webform_access.entity.webform_access_type.schema.yml
@@ -0,0 +1,10 @@
+webform.webform_access_type.*:
+  type: config_entity
+  label: 'Access type'
+  mapping:
+    id:
+      type: string
+      label: 'Machine name'
+    label:
+      type: label
+      label: 'Label'
diff --git a/web/modules/webform/modules/webform_access/config/schema/webform_access.schema.yml b/web/modules/webform/modules/webform_access/config/schema/webform_access.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3dbd332c8d8669dc8887b48dc9da72d80bd02389
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/config/schema/webform_access.schema.yml
@@ -0,0 +1,33 @@
+webform_access.webform_access_type.*:
+  type: config_entity
+  label: 'Access type'
+  mapping:
+    id:
+      type: string
+      label: 'Machine name'
+    label:
+      type: label
+      label: 'Label'
+
+webform_access.webform_access_group.*:
+  type: config_entity
+  label: 'Access group'
+  mapping:
+    id:
+      type: string
+      label: 'Machine name'
+    label:
+      type: label
+      label: 'Label'
+    description:
+      type: label
+      label: 'Administrative description'
+    type:
+      type: string
+      label: 'Type'
+    permissions:
+      type: sequence
+      label: 'Permissions'
+      sequence:
+        type: string
+        label: 'Permission'
diff --git a/web/modules/webform/modules/webform_access/js/webform_access.admin.js b/web/modules/webform/modules/webform_access/js/webform_access.admin.js
new file mode 100644
index 0000000000000000000000000000000000000000..026c651de7bf86e2d6633b51f1825ab5b2a521bd
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/js/webform_access.admin.js
@@ -0,0 +1,36 @@
+/**
+ * @file
+ * JavaScript behaviors for webform access.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * Initialize webform access group administer permission toggle.
+   *
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.webformAccessGroupPermissions = {
+    attach: function (context) {
+      $('#edit-permissions', context).once('webform-access-group-permissions').each(function () {
+        var $permissions = $(this);
+        var $checkbox = $permissions.find('input[name="permissions[administer]"]');
+
+        $checkbox.click(toggleAdminister);
+        if ($checkbox.prop('checked')) {
+          toggleAdminister();
+        }
+
+        function toggleAdminister() {
+          var checked = $checkbox.prop('checked');
+          $permissions.find(':checkbox').prop('checked', checked);
+          $permissions.find(':checkbox:not([name="permissions[administer]"])').attr('disabled', checked);
+        }
+      });
+
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/web/modules/webform/modules/webform_access/src/Breadcrumb/WebformAccessBreadcrumbBuilder.php b/web/modules/webform/modules/webform_access/src/Breadcrumb/WebformAccessBreadcrumbBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..ec7a068f28c4f65f8cfa4c00b60672cd151981c3
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/Breadcrumb/WebformAccessBreadcrumbBuilder.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\webform_access\Breadcrumb;
+
+use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
+use Drupal\Core\Breadcrumb\Breadcrumb;
+use Drupal\Core\Link;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\Core\Url;
+
+/**
+ * Provides a webform access breadcrumb builder.
+ */
+class WebformAccessBreadcrumbBuilder implements BreadcrumbBuilderInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The current route's entity or plugin type.
+   *
+   * @var string
+   */
+  protected $type;
+
+  /**
+   * Constructs a WebformAccessBreadcrumbBuilder.
+   *
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The string translation service.
+   */
+  public function __construct(TranslationInterface $string_translation) {
+    $this->setStringTranslation($string_translation);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(RouteMatchInterface $route_match) {
+    $route_name = $route_match->getRouteName();
+    // All routes must begin or contain 'webform_access'.
+    if (strpos($route_name, 'webform_access') === FALSE) {
+      return FALSE;
+    }
+    $path = Url::fromRouteMatch($route_match)->toString();
+
+    if (strpos($path, 'admin/structure/webform/access/') === FALSE) {
+      return FALSE;
+    }
+
+    if (strpos($path, 'admin/structure/webform/access/group/manage/') !== FALSE) {
+      $this->type = 'webform_access_group';
+    }
+    elseif (strpos($path, 'admin/structure/webform/access/type/manage/') !== FALSE) {
+      $this->type = 'webform_access_type';
+    }
+    else {
+      $this->type = 'webform_access';
+    }
+
+    return ($this->type) ? TRUE : FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build(RouteMatchInterface $route_match) {
+    $breadcrumb = new Breadcrumb();
+    $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
+    $breadcrumb->addLink(Link::createFromRoute($this->t('Administration'), 'system.admin'));
+    $breadcrumb->addLink(Link::createFromRoute($this->t('Structure'), 'system.admin_structure'));
+    $breadcrumb->addLink(Link::createFromRoute($this->t('Webforms'), 'entity.webform.collection'));
+    $breadcrumb->addLink(Link::createFromRoute($this->t('Access'), 'entity.webform_access_group.collection'));
+    switch ($this->type) {
+      case 'webform_access_group':
+        $breadcrumb->addLink(Link::createFromRoute($this->t('Groups'), 'entity.webform_access_group.collection'));
+        break;
+
+      case 'webform_access_type';
+        $breadcrumb->addLink(Link::createFromRoute($this->t('Types'), 'entity.webform_access_type.collection'));
+        break;
+    }
+
+    // This breadcrumb builder is based on a route parameter, and hence it
+    // depends on the 'route' cache context.
+    $breadcrumb->addCacheContexts(['route']);
+
+    return $breadcrumb;
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/Entity/WebformAccessGroup.php b/web/modules/webform/modules/webform_access/src/Entity/WebformAccessGroup.php
new file mode 100644
index 0000000000000000000000000000000000000000..fd3ae8220af39cbe55a9c08b0c0a524976a9c656
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/Entity/WebformAccessGroup.php
@@ -0,0 +1,220 @@
+<?php
+
+namespace Drupal\webform_access\Entity;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\webform_access\WebformAccessGroupInterface;
+
+/**
+ * Defines the webform access group entity.
+ *
+ * @ConfigEntityType(
+ *   id = "webform_access_group",
+ *   label = @Translation("Webform access group"),
+ *   label_collection = @Translation("Access groups"),
+ *   label_singular = @Translation("access group"),
+ *   label_plural = @Translation("access groups"),
+ *   label_count = @PluralTranslation(
+ *     singular = "@count access group",
+ *     plural = "@count access groups",
+ *   ),
+ *   handlers = {
+ *     "storage" = "\Drupal\webform_access\WebformAccessGroupStorage",
+ *     "access" = "Drupal\webform_access\WebformAccessGroupAccessControlHandler",
+ *     "list_builder" = "Drupal\webform_access\WebformAccessGroupListBuilder",
+ *     "form" = {
+ *       "add" = "Drupal\webform_access\WebformAccessGroupForm",
+ *       "edit" = "Drupal\webform_access\WebformAccessGroupForm",
+ *       "duplicate" = "Drupal\webform_access\WebformAccessGroupForm",
+ *       "delete" = "Drupal\webform_access\WebformAccessGroupDeleteForm",
+ *     }
+ *   },
+ *   admin_permission = "administer webform",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label",
+ *   },
+ *   links = {
+ *     "add-form" = "/admin/structure/webform/config/access/group/add",
+ *     "edit-form" = "/admin/structure/webform/config/access/group/manage/{webform_access_group}",
+ *     "duplicate-form" = "/admin/structure/webform/config/access/group/manage/{webform_access_group}/duplicate",
+ *     "delete-form" = "/admin/structure/webform/config/access/group/manage/{webform_access_group}/delete",
+ *     "collection" = "/admin/structure/webform/config/access/group/manage",
+ *   },
+ *   config_export = {
+ *     "id",
+ *     "uuid",
+ *     "label",
+ *     "description",
+ *     "type",
+ *     "permissions",
+ *   }
+ * )
+ */
+class WebformAccessGroup extends ConfigEntityBase implements WebformAccessGroupInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The webform access group ID.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The webform access group UUID.
+   *
+   * @var string
+   */
+  protected $uuid;
+
+  /**
+   * The webform access group label.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * The webform access group description.
+   *
+   * @var string
+   */
+  protected $description;
+
+  /**
+   * The webform access group type.
+   *
+   * @var string
+   */
+  protected $type;
+
+  /**
+   * The webform access group permissions.
+   *
+   * @var array
+   */
+  protected $permissions = [];
+
+  /**
+   * The webform access group user ids.
+   *
+   * @var array
+   */
+  protected $userIds = [];
+
+  /**
+   * The webform access group source entity ids.
+   *
+   * @var array
+   */
+  protected $entityIds = [];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getType() {
+    return $this->type ?: '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTypeLabel() {
+    if (empty($this->type)) {
+      return '';
+    }
+
+    $webform_access_type = WebformAccessType::load($this->type);
+    return ($webform_access_type) ? $webform_access_type->label() : '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUserIds(array $uids) {
+    $this->userIds = $uids;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUserIds() {
+    return $this->userIds;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setEntityIds(array $entity_ids) {
+    $this->entityIds = $entity_ids;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityIds() {
+    return $this->entityIds;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addEntityId($entity_type, $entity_id, $field_name, $webform_id) {
+    $entity = "$entity_type:$entity_id:$field_name:$webform_id";
+    if (!in_array($entity, $this->entityIds)) {
+      $this->entityIds[] = $entity;
+    }
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeEntityId($entity_type, $entity_id, $field_name, $webform_id) {
+    $entity = "$entity_type:$entity_id:$field_name:$webform_id";
+    foreach ($this->entityIds as $index => $entityId) {
+      if ($entity == $entityId) {
+        unset($this->entityIds[$index]);
+      }
+    }
+    array_values($this->entityIds);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addUserId($uid) {
+    if (!in_array($uid, $this->userIds)) {
+      $this->userIds[] = $uid;
+    }
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeUserId($uid) {
+    foreach ($this->userIds as $index => $userId) {
+      if ($userId == $uid) {
+        unset($this->userIds[$index]);
+      }
+    }
+    array_values($this->userIds);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function invalidateTags() {
+    Cache::invalidateTags($this->getCacheTagsToInvalidate());
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/Entity/WebformAccessType.php b/web/modules/webform/modules/webform_access/src/Entity/WebformAccessType.php
new file mode 100644
index 0000000000000000000000000000000000000000..b60a7dbde4387e6d8983b537def8fecd63b97209
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/Entity/WebformAccessType.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\webform_access\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\webform_access\WebformAccessTypeInterface;
+
+/**
+ * Defines the webform access type entity.
+ *
+ * @ConfigEntityType(
+ *   id = "webform_access_type",
+ *   label = @Translation("Webform access type"),
+ *   label_collection = @Translation("Access types"),
+ *   label_singular = @Translation("access type"),
+ *   label_plural = @Translation("access types"),
+ *   label_count = @PluralTranslation(
+ *     singular = "@count access type",
+ *     plural = "@count access types",
+ *   ),
+ *   handlers = {
+ *     "storage" = "\Drupal\webform_access\WebformAccessTypeStorage",
+ *     "access" = "Drupal\webform_access\WebformAccessTypeAccessControlHandler",
+ *     "list_builder" = "Drupal\webform_access\WebformAccessTypeListBuilder",
+ *     "form" = {
+ *       "add" = "Drupal\webform_access\WebformAccessTypeForm",
+ *       "edit" = "Drupal\webform_access\WebformAccessTypeForm",
+ *       "delete" = "Drupal\webform_access\WebformAccessTypeDeleteForm",
+ *     }
+ *   },
+ *   admin_permission = "administer webform",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label",
+ *   },
+ *   links = {
+ *     "add-form" = "/admin/structure/webform/config/access/type/add",
+ *     "edit-form" = "/admin/structure/webform/config/access/type/manage/{webform_access_type}",
+ *     "delete-form" = "/admin/structure/webform/config/access/type/manage/{webform_access_type}/delete",
+ *     "collection" = "/admin/structure/webform/config/access/type/manage",
+ *   },
+ *   config_export = {
+ *     "id",
+ *     "uuid",
+ *     "label",
+ *   }
+ * )
+ */
+class WebformAccessType extends ConfigEntityBase implements WebformAccessTypeInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The webform access type ID.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The webform access type UUID.
+   *
+   * @var string
+   */
+  protected $uuid;
+
+  /**
+   * The webform access type label.
+   *
+   * @var string
+   */
+  protected $label;
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/Plugin/Block/WebformAccessGroupEntityBlock.php b/web/modules/webform/modules/webform_access/src/Plugin/Block/WebformAccessGroupEntityBlock.php
new file mode 100644
index 0000000000000000000000000000000000000000..2ec9c83f2abc69c6e23363f53e35800b52396824
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/Plugin/Block/WebformAccessGroupEntityBlock.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Drupal\webform_access\Plugin\Block;
+
+use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a 'webform_access_group_entity' block.
+ *
+ * @Block(
+ *   id = "webform_access_group_entity",
+ *   admin_label = @Translation("Webform access group entities"),
+ *   category = @Translation("Webform access")
+ * )
+ */
+class WebformAccessGroupEntityBlock extends BlockBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * The webform access group storage.
+   *
+   * @var \Drupal\webform_access\WebformAccessGroupStorageInterface
+   */
+  protected $webformAccessGroupStorage;
+
+  /**
+   * Creates a WebformAccessGroupEntityBlock instance.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->currentUser = $current_user;
+    $this->webformAccessGroupStorage = $entity_type_manager->getStorage('webform_access_group');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('current_user'),
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    /** @var \Drupal\node\NodeInterface[] $nodes */
+    $nodes = $this->webformAccessGroupStorage->getUserEntities($this->currentUser, 'node');
+    if (empty($nodes)) {
+      return NULL;
+    }
+
+    $items = [];
+    foreach ($nodes as $node) {
+      if ($node->access()) {
+        $items[] = $node->toLink()->toRenderable();
+      }
+    }
+    if (empty($items)) {
+      return NULL;
+    }
+
+    return [
+      '#theme' => 'item_list',
+      '#items' => $items,
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    // @todo Setup cache tags and context .
+    return 0;
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/Tests/WebformAccessSubmissionViewsTest.php b/web/modules/webform/modules/webform_access/src/Tests/WebformAccessSubmissionViewsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..065447c7a6eaea7b11d80392ffbdb153648b7cca
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/Tests/WebformAccessSubmissionViewsTest.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\webform_access\Tests;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Entity\WebformSubmission;
+use Drupal\webform\WebformInterface;
+
+/**
+ * Tests for webform access submission views.
+ *
+ * @group WebformAccess
+ */
+class WebformAccessSubmissionViewsTest extends WebformAccessTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['views', 'webform_test_views'];
+
+  /**
+   * Tests webform access submission views.
+   */
+  public function testWebformAccessSubmissionViewsTest() {
+    // Create a test submission for each node and user account.
+    $webform = Webform::load('contact');
+    /** @var \Drupal\webform\WebformSubmissionGenerateInterface $submission_generate */
+    $submission_generate = \Drupal::service('webform_submission.generate');
+    foreach ($this->nodes as $node) {
+      foreach ($this->users as $user) {
+        WebformSubmission::create([
+          'webform_id' => 'contact',
+          'entity_type' => 'node',
+          'entity_id' => $node->id(),
+          'uid' => $user->id(),
+          'data' => $submission_generate->getData($webform),
+        ])->save();
+      }
+    }
+
+    $this->checkUserSubmissionAccess($webform, $this->users);
+  }
+
+  /**
+   * Check user submission access.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   The webform.
+   * @param array $accounts
+   *   An associative array of test users.
+   *
+   * @see \Drupal\Tests\webform\Functional\WebformSubmissionViewsAccessTest::checkUserSubmissionAccess
+   */
+  protected function checkUserSubmissionAccess(WebformInterface $webform, array $accounts) {
+    /** @var \Drupal\webform\WebformSubmissionStorageInterface $webform_submission_storage */
+    $webform_submission_storage = \Drupal::entityTypeManager()->getStorage('webform_submission');
+
+    // Reset the static cache to make sure we are hitting actual fresh access
+    // results.
+    \Drupal::entityTypeManager()->getStorage('webform_submission')->resetCache();
+    \Drupal::entityTypeManager()->getAccessControlHandler('webform_submission')->resetCache();
+
+    foreach ($accounts as $account_type => $account) {
+      // Login the current user.
+      $this->drupalLogin($account);
+
+      // Get the webform_test_views_access view and the sid for each
+      // displayed record.  Submission access is controlled via the query.
+      // @see webform_query_webform_submission_access_alter()
+      $this->drupalGet('/admin/structure/webform/test/views_access');
+
+      $views_sids = [];
+      foreach ($this->cssSelect('.view .view-content tbody .views-field-sid') as $node) {
+        $views_sids[] = $node->getText();
+      }
+      sort($views_sids);
+
+      $expected_sids = [];
+
+      // Load all webform submissions and check access using the access method.
+      // @see \Drupal\webform\WebformSubmissionAccessControlHandler::checkAccess
+      $webform_submissions = $webform_submission_storage->loadByEntities($webform);
+
+      foreach ($webform_submissions as $webform_submission) {
+        if ($webform_submission->access('view', $account)) {
+          $expected_sids[] = $webform_submission->id();
+        }
+      }
+
+      sort($expected_sids);
+
+      // Check that the views sids is equal to the expected sids.
+      $this->assertEqual($expected_sids, $views_sids, "User '" . $account_type . "' access has correct access through view on webform submission entity type.");
+    }
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/Tests/WebformAccessTest.php b/web/modules/webform/modules/webform_access/src/Tests/WebformAccessTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..43c8520a15bbb179bda5915d6d43e88e9fe9cc39
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/Tests/WebformAccessTest.php
@@ -0,0 +1,165 @@
+<?php
+
+namespace Drupal\webform_access\Tests;
+
+use Drupal\field\Entity\FieldConfig;
+
+/**
+ * Tests for webform access.
+ *
+ * @group WebformAccess
+ */
+class WebformAccessTest extends WebformAccessTestBase {
+
+  /**
+   * Tests webform access.
+   */
+  public function testWebformAccess() {
+    $nid = $this->nodes['contact_01']->id();
+
+    $this->drupalLogin($this->rootUser);
+
+    // Check that employee and manager groups exist.
+    $this->drupalGet('/admin/structure/webform/access/group/manage');
+    $this->assertLink('employee_group');
+    $this->assertLink('manager_group');
+
+    // Check that webform node is assigned to groups.
+    $this->assertLink($this->nodes['contact_01']->label());
+
+    // Check that employee and manager users can't access webform results.
+    foreach ($this->users as $account) {
+      $this->drupalLogin($account);
+      $this->drupalGet("/node/$nid/webform/results/submissions");
+      $this->assertResponse(403);
+    }
+
+    // Assign users to groups via the UI.
+    $this->drupalLogin($this->rootUser);
+    foreach ($this->groups as $name => $group) {
+      $this->drupalPostForm(
+        "/admin/structure/webform/access/group/manage/$name",
+        ['users[]' => $this->users[$name]->id()],
+        t('Save')
+      );
+    }
+
+    // Check that manager and employee users can access webform results.
+    foreach (['manager', 'employee'] as $name) {
+      $account = $this->users[$name];
+      $this->drupalLogin($account);
+      $this->drupalGet("/node/$nid/webform/results/submissions");
+      $this->assertResponse(200);
+    }
+
+    // Check that employee can't delete results.
+    $this->drupalLogin($this->users['employee']);
+    $this->drupalGet("/node/$nid/webform/results/clear");
+    $this->assertResponse(403);
+
+    // Check that manager can delete results.
+    $this->drupalLogin($this->users['manager']);
+    $this->drupalGet("/node/$nid/webform/results/clear");
+    $this->assertResponse(200);
+
+    // Unassign employee user from employee group via the UI.
+    $this->drupalLogin($this->rootUser);
+    $this->drupalPostForm(
+      '/admin/structure/webform/access/group/manage/employee',
+      ['users[]' => 1],
+      t('Save')
+    );
+
+    // Assign employee user to manager group via the UI.
+    $this->drupalLogin($this->rootUser);
+    $this->drupalPostForm(
+      '/user/' . $this->users['employee']->id() . '/edit',
+      ['webform_access_group[]' => 'manager'],
+      t('Save')
+    );
+
+    // Check that employee can now delete results.
+    $this->drupalLogin($this->users['employee']);
+    $this->drupalGet("/node/$nid/webform/results/clear");
+    $this->assertResponse(200);
+
+    // Unassign node from groups.
+    $this->drupalLogin($this->rootUser);
+    foreach ($this->groups as $name => $group) {
+      $this->drupalPostForm(
+        "/admin/structure/webform/access/group/manage/$name",
+        ['entities[]' => 'node:' . $this->nodes['contact_02']->id() . ':webform:contact'],
+        t('Save')
+      );
+    }
+
+    // Check that employee can't access results.
+    $this->drupalLogin($this->users['employee']);
+    $this->drupalGet("/node/$nid/webform/results/clear");
+    $this->assertResponse(403);
+
+    // Assign webform node to group via the UI.
+    $this->drupalLogin($this->rootUser);
+    $this->drupalPostForm(
+      "/node/$nid/edit",
+      ['webform[0][settings][webform_access_group][]' => 'manager'],
+      t('Save')
+    );
+
+    // Check that employee can now access results.
+    $this->drupalLogin($this->users['employee']);
+    $this->drupalGet("/node/$nid/webform/results/clear");
+    $this->assertResponse(200);
+
+    // Delete employee group.
+    $this->groups['employee']->delete();
+
+    // Check that employee group is configured.
+    $this->drupalLogin($this->rootUser);
+    $this->drupalGet('/admin/structure/webform/access/group/manage');
+    $this->assertRaw('manager_type');
+    $this->assertLink('manager_group');
+    $this->assertLink('manager_user');
+    $this->assertLink('employee_user');
+    $this->assertLink('contact_01');
+    $this->assertLink('contact_02');
+
+    // Reset caches.
+    \Drupal::entityTypeManager()->getStorage('webform_access_group')->resetCache();
+    \Drupal::entityTypeManager()->getStorage('webform_access_type')->resetCache();
+
+    // Delete types.
+    foreach ($this->types as $type) {
+      $type->delete();
+    }
+
+    // Check that manager type has been removed.
+    $this->drupalGet('/admin/structure/webform/access/group/manage');
+    $this->assertNoRaw('manager_type');
+
+    // Delete users.
+    foreach ($this->users as $user) {
+      $user->delete();
+    }
+
+    // Check that manager type has been removed.
+    $this->drupalGet('/admin/structure/webform/access/group/manage');
+    $this->assertNoLink('manager_user');
+    $this->assertNoLink('employee_user');
+
+    // Delete contact 2.
+    $this->nodes['contact_02']->delete();
+
+    // Check that contact_02 has been removed.
+    $this->drupalGet('/admin/structure/webform/access/group/manage');
+    $this->assertNoLink('contact_02');
+
+    // Delete webform field config.
+    FieldConfig::loadByName('node', 'webform', 'webform')->delete();
+
+    // Check that contact_02 has been removed.
+    $this->drupalGet('/admin/structure/webform/access/group/manage');
+    $this->assertNoLink('contact_02');
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/Tests/WebformAccessTestBase.php b/web/modules/webform/modules/webform_access/src/Tests/WebformAccessTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..a1069fe872a04f2130d259f9584e91488921ad20
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/Tests/WebformAccessTestBase.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\webform_access\Tests;
+
+use Drupal\webform_access\Entity\WebformAccessGroup;
+use Drupal\webform_access\Entity\WebformAccessType;
+use Drupal\webform_node\Tests\WebformNodeTestBase;
+
+/**
+ * Test base for webform access.
+ */
+abstract class WebformAccessTestBase extends WebformNodeTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'webform_access'];
+
+  /**
+   * Webform node[].
+   *
+   * @var \Drupal\node\NodeInterface
+   */
+  protected $nodes = [];
+
+  /**
+   * Users.
+   *
+   * @var \Drupal\user\UserInterface[]
+   */
+  protected $users = [];
+
+  /**
+   * Access types (manager, employee, and customer).
+   *
+   * @var \Drupal\webform_access\WebformAccessTypeInterface[]
+   */
+  protected $types = [];
+
+  /**
+   * Access groups (manager, employee, and customer).
+   *
+   * @var \Drupal\webform_access\WebformAccessGroupInterface[]
+   */
+  protected $groups = [];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Create webform nodes.
+    $this->nodes['contact_01'] = $this->createWebformNode('contact', ['title' => 'contact_01']);
+    $this->nodes['contact_02'] = $this->createWebformNode('contact', ['title' => 'contact_02']);
+
+    // Create webform access types and groups.
+    $types = [
+      'manager' => [
+        'administer',
+      ],
+      'employee' => [
+        'view_any',
+        'update_any',
+      ],
+      'customer' => [
+        'view_own',
+        'update_own',
+      ],
+    ];
+    foreach ($types as $type => $permissions) {
+      $this->users[$type] = $this->drupalCreateUser([], $type . '_user');
+
+      $values = [
+        'id' => $type,
+        'label' => $type . '_type',
+      ];
+      $webform_access_type = WebformAccessType::create($values);
+      $webform_access_type->save();
+      $this->types[$type] = $webform_access_type;
+
+      $values = [
+        'id' => $type,
+        'type' => $type,
+        'label' => $type . '_group',
+        'permissions' => $permissions,
+      ];
+      $webform_access_group = WebformAccessGroup::create($values);
+      $webform_access_group->addEntityId('node', $this->nodes['contact_01']->id(), 'webform', 'contact');
+      $webform_access_group->save();
+      $this->groups[$type] = $webform_access_group;
+    }
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/Tests/WebformAccessTokensTest.php b/web/modules/webform/modules/webform_access/src/Tests/WebformAccessTokensTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..78768ea32d8b2b5b9cedaed02daa67cb7e435c30
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/Tests/WebformAccessTokensTest.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\webform_access\Tests;
+
+use Drupal\webform\Entity\WebformSubmission;
+
+/**
+ * Tests for webform tokens access.
+ *
+ * @group WebformAccess
+ */
+class WebformAccessTokensTest extends WebformAccessTestBase {
+
+  /**
+   * Tests webform access tokens.
+   */
+  public function testWebformAccessTokens() {
+    // Add both users to employee group.
+    foreach ($this->users as $account) {
+      $this->groups['employee']->addUserId($account->id());
+    }
+    $this->groups['employee']->save();
+    $this->users['other'] = $this->drupalCreateUser([], 'other_user');
+    $this->groups['manager']->setUserIds([$this->users['other']->id()]);
+    $this->groups['manager']->save();
+
+    // Create a submission.
+    $edit = [
+      'name' => 'name',
+      'email' => 'name@example.com',
+      'subject' => 'subject',
+      'message' => 'message',
+    ];
+    $sid = $this->postNodeSubmission($this->nodes['contact_01'], $edit);
+    $webform_submission = WebformSubmission::load($sid);
+
+    /** @var \Drupal\webform\WebformTokenManagerInterface $token_manager */
+    $token_manager = \Drupal::service('webform.token_manager');
+    $token_data['webform_access'] = $webform_submission;
+
+    // Check [webform_access:type:employee] token.
+    $result = $token_manager->replace('[webform_access:type:employee]', $webform_submission, $token_data);
+    $this->assertEqual('customer_user@example.com,employee_user@example.com,manager_user@example.com', $result);
+
+    // Check [webform_access:type:manager] token.
+    $result = $token_manager->replace('[webform_access:type:manager]', $webform_submission, $token_data);
+    $this->assertEqual('other_user@example.com', $result);
+
+    // Check [webform_access:type:all] token.
+    $result = $token_manager->replace('[webform_access:type]', $webform_submission, $token_data);
+    $this->assertEqual('customer_user@example.com,employee_user@example.com,manager_user@example.com,other_user@example.com', $result);
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessGroupAccessControlHandler.php b/web/modules/webform/modules/webform_access/src/WebformAccessGroupAccessControlHandler.php
new file mode 100644
index 0000000000000000000000000000000000000000..f307bc3fb874237dab0a42301f9308712b6b3bd1
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessGroupAccessControlHandler.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the access control handler for the webform access entity type.
+ *
+ * @see \Drupal\webform_access\Entity\WebformAccessGroup.
+ */
+class WebformAccessGroupAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    return AccessResult::allowedIfHasPermission($account, 'administer webform');
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessGroupDeleteForm.php b/web/modules/webform/modules/webform_access/src/WebformAccessGroupDeleteForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..c51b26a65becb29e6ac6db6cd2ad415b9a1376f4
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessGroupDeleteForm.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\webform\Form\WebformConfigEntityDeleteFormBase;
+
+/**
+ * Provides a delete webform access group form.
+ */
+class WebformAccessGroupDeleteForm extends WebformConfigEntityDeleteFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $confirmCheckbox = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return [
+      'title' => [
+        '#markup' => $this->t('This action will…'),
+      ],
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => [
+          $this->t('Remove configuration'),
+          $this->t('Affect any fields which use this access group'),
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessGroupForm.php b/web/modules/webform/modules/webform_access/src/WebformAccessGroupForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..664544aa7d4d4e5bf576f443b7dc064238e10ad7
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessGroupForm.php
@@ -0,0 +1,317 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Plugin\WebformElementManagerInterface;
+use Drupal\webform\Utility\WebformDialogHelper;
+use Drupal\webform\WebformAccessRulesManagerInterface;
+use Drupal\webform\WebformEntityReferenceManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Entity\EntityManager;
+
+/**
+ * Provides a form to define a webform access group.
+ */
+class WebformAccessGroupForm extends EntityForm {
+
+  /**
+   * The database object.
+   *
+   * @var object
+   */
+  protected $database;
+
+  /**
+   * Entity manager.
+   *
+   * @var Drupal\Core\Entity\EntityManager
+   */
+  protected $entityManager;
+
+  /**
+   * The webform element manager.
+   *
+   * @var \Drupal\webform\Plugin\WebformElementManagerInterface
+   */
+  protected $elementManager;
+
+  /**
+   * The webform entity reference manager.
+   *
+   * @var \Drupal\webform\WebformEntityReferenceManagerInterface
+   */
+  protected $webformEntityReferenceManager;
+
+  /**
+   * The webform access rules manager.
+   *
+   * @var \Drupal\webform\WebformAccessRulesManagerInterface
+   */
+  protected $webformAccessRulesManager;
+
+  /**
+   * Constructs a WebformAccessGroupForm.
+   *
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database.
+   * @param \Drupal\Core\Entity\EntityManager $entity_manager
+   *   The entity manager.
+   * @param \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager
+   *   The webform element manager.
+   * @param \Drupal\webform\WebformEntityReferenceManagerInterface $webform_entity_reference_manager
+   *   The webform entity reference manager.
+   * @param \Drupal\webform\WebformAccessRulesManagerInterface $webform_access_rules_manager
+   *   The webform access rules manager.
+   */
+  public function __construct(Connection $database, EntityManager $entity_manager, WebformElementManagerInterface $element_manager, WebformEntityReferenceManagerInterface $webform_entity_reference_manager, WebformAccessRulesManagerInterface $webform_access_rules_manager) {
+    $this->database = $database;
+    $this->entityManager = $entity_manager;
+    $this->elementManager = $element_manager;
+    $this->webformEntityReferenceManager = $webform_entity_reference_manager;
+    $this->webformAccessRulesManager = $webform_access_rules_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('database'),
+      $container->get('entity.manager'),
+      $container->get('plugin.manager.webform.element'),
+      $container->get('webform.entity_reference_manager'),
+      $container->get('webform.access_rules_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareEntity() {
+
+    if ($this->operation == 'duplicate') {
+      $this->setEntity($this->getEntity()->createDuplicate());
+    }
+
+    parent::prepareEntity();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\webform_access\WebformAccessGroupInterface $webform_access_group */
+    $webform_access_group = $this->getEntity();
+
+    // Customize title for duplicate and edit operation.
+    switch ($this->operation) {
+      case 'duplicate':
+        $form['#title'] = $this->t("Duplicate '@label' access group", ['@label' => $webform_access_group->label()]);
+        break;
+
+      case 'edit':
+        $form['#title'] = $webform_access_group->label();
+        break;
+    }
+
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\webform_access\WebformAccessGroupInterface $webform_access_group */
+    $webform_access_group = $this->entity;
+
+    $form['label'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Label'),
+      '#maxlength' => 255,
+      '#required' => TRUE,
+      '#attributes' => ($webform_access_group->isNew()) ? ['autofocus' => 'autofocus'] : [],
+      '#default_value' => $webform_access_group->label(),
+    ];
+    $form['id'] = [
+      '#type' => 'machine_name',
+      '#machine_name' => [
+        'exists' => '\Drupal\webform_access\Entity\WebformAccessGroup::load',
+        'label' => '<br/>' . $this->t('Machine name'),
+      ],
+      '#maxlength' => 32,
+      '#field_suffix' => ' (' . $this->t('Maximum @max characters', ['@max' => 32]) . ')',
+      '#required' => TRUE,
+      '#disabled' => !$webform_access_group->isNew(),
+      '#default_value' => $webform_access_group->id(),
+    ];
+    $form['description'] = [
+      '#type' => 'webform_html_editor',
+      '#title' => $this->t('Description'),
+      '#default_value' => $webform_access_group->get('description'),
+    ];
+    $form['type'] = [
+      '#type' => 'webform_entity_select',
+      '#title' => $this->t('Type'),
+      '#target_type' => 'webform_access_type',
+      '#empty_option' => $this->t('- None -'),
+      '#default_value' => $webform_access_group->get('type'),
+    ];
+
+    // Users.
+    $form['users'] = [
+      '#type' => 'webform_entity_select',
+      '#title' => $this->t('Users'),
+      '#target_type' => 'user',
+      '#multiple' => TRUE,
+      '#selection_handler' => 'default:user',
+      '#selection_settings' => [
+        'include_anonymous' => FALSE,
+      ],
+      '#select2' => TRUE,
+      '#default_value' => $webform_access_group->getUserIds(),
+
+    ];
+    $this->elementManager->processElement($form['users']);
+
+    // Entities (Nodes).
+    $form['entities'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Nodes'),
+      '#multiple' => TRUE,
+      '#select2' => TRUE,
+      '#options' => $this->getEntitiesAsOptions(),
+      '#default_value' => $webform_access_group->getEntityIds(),
+    ];
+    $this->elementManager->processElement($form['entities']);
+
+    // Permissions.
+    $permissions_options = [];
+    $access_rules = $this->webformAccessRulesManager->getAccessRulesInfo();
+    foreach ($access_rules as $permission => $access_rule) {
+      $permissions_options[$permission] = [
+        'title' => $access_rule['title'],
+      ];
+    }
+    $form['permissions_label'] = [
+      '#type' => 'label',
+      '#title' => $this->t('Permissions'),
+    ];
+    $form['permissions'] = [
+      '#type' => 'tableselect',
+      '#header' => ['title' => $this->t('Permission')],
+      '#js_select' => FALSE,
+      '#options' => $permissions_options,
+      '#default_value' => $webform_access_group->get('permissions'),
+    ];
+    $this->elementManager->processElement($form['permissions']);
+
+    $form['#attached']['library'][] = 'webform_access/webform_access.admin';
+
+    return parent::form($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, FormStateInterface $form_state) {
+    $actions = parent::actions($form, $form_state);
+
+    // Open delete button in a modal dialog.
+    if (isset($actions['delete'])) {
+      $actions['delete']['#attributes'] = WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW, $actions['delete']['#attributes']['class']);
+      WebformDialogHelper::attachLibraries($actions['delete']);
+    }
+
+    return $actions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $form_state->setValue('permissions', array_filter($form_state->getValue('permissions')));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\webform_access\WebformAccessGroupInterface $webform_access_group */
+    $webform_access_group = $this->getEntity();
+    $webform_access_group->setUserIds($form_state->getValue('users'));
+    $webform_access_group->setEntityIds($form_state->getValue('entities'));
+    $webform_access_group->save();
+
+    // Log and display message.
+    $context = [
+      '@label' => $webform_access_group->label(),
+      'link' => $webform_access_group->toLink($this->t('Edit'), 'edit-form')->toString(),
+    ];
+    $this->logger('webform')->notice('Access group @label saved.', $context);
+    $this->messenger()->addStatus($this->t('Access group %label saved.', ['%label' => $webform_access_group->label()]));
+
+    // Redirect to list.
+    $form_state->setRedirect('entity.webform_access_group.collection');
+  }
+
+  /**
+   * Get webform entities as options.
+   *
+   * @return array
+   *   An associative array container webform node options.
+   */
+  protected function getEntitiesAsOptions() {
+    // Collects webform nodes.
+    $webform_nodes = [];
+    $nids = [];
+    $webform_ids = [];
+
+    $table_names = $this->webformEntityReferenceManager->getTableNames();
+    foreach ($table_names as $table_name => $field_name) {
+      if (strpos($table_name, 'node_revision__') !== 0) {
+        continue;
+      }
+      $query = $this->database->select($table_name, 'n');
+      $query->distinct();
+      $query->fields('n', ['entity_id', $field_name . '_target_id']);
+      $query->condition($field_name . '_target_id', '', '<>');
+      $query->isNotNull($field_name . '_target_id');
+      $result = $query->execute()->fetchAllKeyed();
+      foreach ($result as $nid => $webform_id) {
+        $webform_nodes[$nid][$field_name][$webform_id] = $webform_id;
+        $webform_ids[$webform_id] = $webform_id;
+        $nids[$nid] = $nid;
+      }
+    }
+
+    /** @var \Drupal\webform\WebformInterface[] $webforms */
+    $webforms = Webform::loadMultiple($webform_ids);
+
+    /** @var \Drupal\node\NodeInterface[] $nodes */
+    $nodes = $this->entityManager->getStorage('node')->loadMultiple($nids);
+
+    $options = [];
+    foreach ($webform_nodes as $nid => $field_names) {
+      if (!isset($nodes[$nid])) {
+        continue;
+      }
+      $node = $nodes[$nid];
+      foreach ($field_names as $field_name => $webform_ids) {
+        foreach ($webform_ids as $webform_id) {
+          if (!isset($webforms[$webform_id])) {
+            continue;
+          }
+          $webform = $webforms[$webform_id];
+          $options['node:' . $node->id() . ':' . $field_name . ':' . $webform->id()] = $node->label() . ': ' . $webform->label();
+        }
+      }
+    }
+    asort($options);
+    return $options;
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessGroupInterface.php b/web/modules/webform/modules/webform_access/src/WebformAccessGroupInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..644cfa5b4904b112e410f2e738a5cba63f4d9eda
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessGroupInterface.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+
+/**
+ * Provides an interface defining a webform access group entity.
+ */
+interface WebformAccessGroupInterface extends ConfigEntityInterface {
+
+  /**
+   * Get webform access group type.
+   *
+   * @return string
+   *   The webform access group type.
+   */
+  public function getType();
+
+  /**
+   * Get webform access group type label.
+   *
+   * @return string
+   *   The webform access group type label.
+   */
+  public function getTypeLabel();
+
+  /**
+   * Set user ids assigned to webform access group.
+   *
+   * @param array $uids
+   *   An array of user ids.
+   *
+   * @return $this
+   */
+  public function setUserIds(array $uids);
+
+  /**
+   * Get user ids assigned to webform access group.
+   *
+   * @return array
+   *   An array of user ids.
+   */
+  public function getUserIds();
+
+  /**
+   * Set entities assigned to webform access group.
+   *
+   * @param array $entity_ids
+   *   An array of entity ids.
+   *   Formatted as 'node:type:field_name:webform'.
+   *
+   * @return $this
+   */
+  public function setEntityIds(array $entity_ids);
+
+  /**
+   * Get entities assigned to webform access group.
+   *
+   * @return array
+   *   An array of entity ids.
+   *   Formatted as 'node:type:field_name:webform'
+   */
+  public function getEntityIds();
+
+  /**
+   * Add entity id to webform access group.
+   *
+   * @param string $entity_type
+   *   The source entity type.
+   * @param string $entity_id
+   *   The source entity id.
+   * @param string $field_name
+   *   The source entity webform field name.
+   * @param string $webform_id
+   *   The webform id.
+   */
+  public function addEntityId($entity_type, $entity_id, $field_name, $webform_id);
+
+  /**
+   * Remove entity id to webform access group.
+   *
+   * @param string $entity_type
+   *   The source entity type.
+   * @param string $entity_id
+   *   The source entity id.
+   * @param string $field_name
+   *   The source entity webform field name.
+   * @param string $webform_id
+   *   The webform id.
+   */
+  public function removeEntityId($entity_type, $entity_id, $field_name, $webform_id);
+
+  /**
+   * Add user id to webform access group.
+   *
+   * @param int $uid
+   *   A user id.
+   *
+   * @return $this
+   */
+  public function addUserId($uid);
+
+  /**
+   * Remove user id to webform access group.
+   *
+   * @param int $uid
+   *   A user id.
+   *
+   * @return $this
+   */
+  public function removeUserId($uid);
+
+  /**
+   * Invalidates an entity's cache tags upon save.
+   */
+  public function invalidateTags();
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessGroupListBuilder.php b/web/modules/webform/modules/webform_access/src/WebformAccessGroupListBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..694f35de459171cb5f5db8ab46d40d5efb1ae951
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessGroupListBuilder.php
@@ -0,0 +1,210 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Url;
+use Drupal\user\Entity\User;
+use Drupal\webform\Element\WebformHtmlEditor;
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Utility\WebformDialogHelper;
+
+/**
+ * Defines a class to build a listing of webform access group entities.
+ *
+ * @see \Drupal\webform\Entity\WebformOption
+ */
+class WebformAccessGroupListBuilder extends ConfigEntityListBuilder {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $limit = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    $build = [];
+
+    // Filter form.
+    $build['filter_form'] = $this->buildFilterForm();
+
+    // Display info.
+    $build['info'] = $this->buildInfo();
+
+    // Table.
+    $build += parent::render();
+    $build['table']['#sticky'] = TRUE;
+    $build['table']['#attributes']['class'][] = 'webform-access-group-table';
+
+    // Attachments.
+    $build['#attached']['library'][] = 'webform/webform.admin.dialog';
+
+    return $build;
+  }
+
+  /**
+   * Build the filter form.
+   *
+   * @return array
+   *   A render array representing the filter form.
+   */
+  protected function buildFilterForm() {
+    return [
+      '#type' => 'search',
+      '#title' => $this->t('Filter'),
+      '#title_display' => 'invisible',
+      '#size' => 30,
+      '#placeholder' => $this->t('Filter by keyword.'),
+      '#attributes' => [
+        'class' => ['webform-form-filter-text'],
+        'data-element' => '.webform-access-group-table',
+        'data-summary' => '.webform-access-group-summary',
+        'data-item-singlular' => $this->t('access group'),
+        'data-item-plural' => $this->t('access groups'),
+        'title' => $this->t('Enter a keyword to filter by.'),
+        'autofocus' => 'autofocus',
+      ],
+    ];
+  }
+
+  /**
+   * Build information summary.
+   *
+   * @return array
+   *   A render array representing the information summary.
+   */
+  protected function buildInfo() {
+    $total = $this->getStorage()->getQuery()->count()->execute();
+    if (!$total) {
+      return [];
+    }
+
+    return [
+      '#markup' => $this->formatPlural($total, '@total access group', '@total access groups', ['@total' => $total]),
+      '#prefix' => '<div class="webform-access-group-summary">',
+      '#suffix' => '</div>',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header = [];
+    $header['label'] = $this->t('Label/Description');
+    $header['type'] = [
+      'data' => $this->t('Type'),
+      'class' => [RESPONSIVE_PRIORITY_MEDIUM],
+    ];
+    $header['users'] = [
+      'data' => $this->t('Users'),
+      'class' => [RESPONSIVE_PRIORITY_LOW],
+    ];
+    $header['entities'] = [
+      'data' => $this->t('Nodes'),
+      'class' => [RESPONSIVE_PRIORITY_LOW],
+    ];
+    $header['permissions'] = [
+      'data' => $this->t('Permissions'),
+      'class' => [RESPONSIVE_PRIORITY_LOW],
+    ];
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    /** @var \Drupal\webform_access\WebformAccessGroupInterface $entity */
+
+    // Label/Description.
+    $row['label'] = [
+      'data' => [
+        'label' => $entity->toLink($entity->label(), 'edit-form')->toRenderable() + ['#suffix' => '<br/>'],
+        'description' => WebformHtmlEditor::checkMarkup($entity->get('description')),
+      ],
+    ];
+
+    // Type.
+    $row['type'] = $entity->getTypeLabel();
+
+    // Users.
+    $uids = $entity->getUserIds();
+    /** @var \Drupal\user\UserInterface[] $users */
+    $users = $uids ? User::loadMultiple($uids) : [];
+    $items = [];
+    foreach ($users as $user) {
+      $items[] = $user->toLink();
+    }
+    $row['users'] = ['data' => ['#theme' => 'item_list', '#items' => $items]];
+
+    // Entities.
+    $source_entities = $entity->getEntityIds();
+    $items = [];
+    foreach ($source_entities as $source_entity_record) {
+      list($source_entity_type, $source_entity_id, $field_name, $webform_id) = explode(':', $source_entity_record);
+      $source_entity = \Drupal::entityManager()->getStorage($source_entity_type)->load($source_entity_id);
+      $webform = Webform::load($webform_id);
+      if ($source_entity && $webform) {
+        $items[] = [
+          'source_entity' => $source_entity->toLink()->toRenderable(),
+          'webform' => ['#prefix' => '<br/>', '#markup' => $webform->label()],
+        ];
+      }
+    }
+    $row['entities'] = ['data' => ['#theme' => 'item_list', '#items' => $items]];
+
+    // Permissions.
+    $permissions = array_intersect_key([
+      'create' => $this->t('Create submissions'),
+      'view_any' => $this->t('View any submissions'),
+      'update_any' => $this->t('Update any submissions'),
+      'delete_any' => $this->t('Delete any submissions'),
+      'purge_any' => $this->t('Purge any submissions'),
+      'view_own' => $this->t('View own submissions'),
+      'update_own' => $this->t('Update own submissions'),
+      'delete_own' => $this->t('Delete own submissions'),
+      'administer' => $this->t('Administer submissions'),
+      'test' => $this->t('Test webform'),
+    ], array_flip($entity->get('permissions')));
+    $row['permissions'] = ['data' => ['#theme' => 'item_list', '#items' => $permissions]];
+    $row = $row + parent::buildRow($entity);
+
+    return [
+      'data' => $row,
+      'class' => ['webform-form-filter-text-source'],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOperations(EntityInterface $entity, $type = 'edit') {
+    $operations = parent::getDefaultOperations($entity);
+    if ($entity->access('duplicate')) {
+      $operations['duplicate'] = [
+        'title' => $this->t('Duplicate'),
+        'weight' => 23,
+        'url' => Url::fromRoute('entity.webform_access_group.duplicate_form', ['webform_access_group' => $entity->id()]),
+      ];
+    }
+    if (isset($operations['delete'])) {
+      $operations['delete']['attributes'] = WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW);
+    }
+    return $operations;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOperations(EntityInterface $entity) {
+    return parent::buildOperations($entity) + [
+      '#prefix' => '<div class="webform-dropbutton">',
+      '#suffix' => '</div>',
+    ];
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessGroupStorage.php b/web/modules/webform/modules/webform_access/src/WebformAccessGroupStorage.php
new file mode 100644
index 0000000000000000000000000000000000000000..378e90c21ee18a71af9543aff5aaa19e58aa7a6f
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessGroupStorage.php
@@ -0,0 +1,227 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\Component\Uuid\UuidInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\Entity\ConfigEntityStorage;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\webform\WebformInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Storage controller class for "webform_access_group" configuration entities.
+ */
+class WebformAccessGroupStorage extends ConfigEntityStorage implements WebformAccessGroupStorageInterface {
+
+  /**
+   * Active database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a WebformAccessGroupStorage object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory service.
+   * @param \Drupal\Component\Uuid\UuidInterface $uuid_service
+   *   The UUID service.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database connection to be used.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, Connection $database, EntityTypeManagerInterface $entity_type_manager) {
+    parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager);
+    $this->database = $database;
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('config.factory'),
+      $container->get('uuid'),
+      $container->get('language_manager'),
+      $container->get('database'),
+      $container->get('entity_type.manager')
+
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doLoadMultiple(array $ids = NULL) {
+    /** @var \Drupal\webform_access\WebformAccessGroupInterface[] $webform_access_groups */
+    $webform_access_groups = parent::doLoadMultiple($ids);
+
+    // Load users.
+    $result = $this->database->select('webform_access_group_user', 'gu')
+      ->fields('gu', ['group_id', 'uid'])
+      ->condition('group_id', $ids, 'IN')
+      ->orderBy('group_id')
+      ->orderBy('uid')
+      ->execute();
+    $users = [];
+    while ($record = $result->fetchAssoc()) {
+      $users[$record['group_id']][] = $record['uid'];
+    }
+    foreach ($webform_access_groups as $group_id => $webform_access_group) {
+      $webform_access_group->setUserIds((isset($users[$group_id])) ? $users[$group_id] : []);
+    }
+
+    // Load entities.
+    $result = $this->database->select('webform_access_group_entity', 'ge')
+      ->fields('ge', ['group_id', 'entity_type', 'entity_id', 'field_name', 'webform_id'])
+      ->condition('group_id', $ids, 'IN')
+      ->orderBy('group_id')
+      ->execute();
+    $entities = [];
+    while ($record = $result->fetchAssoc()) {
+      $group_id = $record['group_id'];
+      unset($record['group_id']);
+      $entities[$group_id][] = implode(':', $record);
+    }
+    foreach ($webform_access_groups as $group_id => $webform_access_group) {
+      $webform_access_group->setEntityIds((isset($entities[$group_id])) ? $entities[$group_id] : []);
+    }
+
+    return $webform_access_groups;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doSave($id, EntityInterface $entity) {
+    /** @var \Drupal\webform_access\WebformAccessGroupInterface $entity */
+    $result = parent::doSave($id, $entity);
+
+    // Save users.
+    $users = $entity->getUserIds();
+    $this->database->delete('webform_access_group_user')
+      ->condition('group_id', $entity->id())
+      ->execute();
+    $query = $this->database
+      ->insert('webform_access_group_user')
+      ->fields(['group_id', 'uid']);
+    $values = ['group_id' => $entity->id()];
+    foreach ($users as $uid) {
+      $values['uid'] = $uid;
+      $query->values($values);
+    }
+    $query->execute();
+
+    // Save entities.
+    $entities = $entity->getEntityIds();
+    $this->database->delete('webform_access_group_entity')
+      ->condition('group_id', $entity->id())
+      ->execute();
+    $query = $this->database
+      ->insert('webform_access_group_entity')
+      ->fields(['group_id', 'entity_type', 'entity_id', 'field_name', 'webform_id']);
+    $values = ['group_id' => $entity->id()];
+    foreach ($entities as $entity) {
+      list($values['entity_type'], $values['entity_id'], $values['field_name'], $values['webform_id']) = explode(':', $entity);
+      $query->values($values);
+    }
+    $query->execute();
+
+    return $result;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete(array $entities) {
+    /** @var \Drupal\webform_access\WebformAccessGroupInterface[] $entities */
+    foreach ($entities as $entity) {
+      $this->database->delete('webform_access_group_entity')
+        ->condition('group_id', $entity->id())
+        ->execute();
+      $this->database->delete('webform_access_group_user')
+        ->condition('group_id', $entity->id())
+        ->execute();
+    }
+    return parent::delete($entities);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadByEntities(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $type = NULL) {
+    $query = $this->database->select('webform_access_group_entity', 'ge');
+    $query->fields('ge', ['group_id']);
+
+    // Webform.
+    if ($webform) {
+      $query->condition('webform_id', $webform->id());
+    }
+    // Source entity.
+    if ($source_entity) {
+      $query->condition('entity_type', $source_entity->getEntityTypeId());
+      $query->condition('entity_id', $source_entity->id());
+    }
+    // Account.
+    if ($account) {
+      $query->innerjoin('webform_access_group_user', 'gu', 'ge.group_id = gu.group_id');
+      $query->condition('uid', $account->id());
+    }
+    // Webform access type.
+    if ($type) {
+      $type_group_ids = $this->getQuery()
+        ->condition('type', $type)
+        ->accessCheck(FALSE)
+        ->execute();
+      if (empty($type_group_ids)) {
+        return [];
+      }
+      $query->condition('group_id', $type_group_ids, 'IN');
+    }
+
+    $group_ids = $query->execute()->fetchCol();
+    return $group_ids ? $this->loadMultiple($group_ids) : [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUserEntities(AccountInterface $account, $entity_type = NULL) {
+    /** @var \Drupal\webform_access\WebformAccessGroupInterface[] $webform_access_groups */
+    $webform_access_groups = $this->loadByEntities(NULL, NULL, $account);
+
+    $source_entity_ids = [];
+    foreach ($webform_access_groups as $webform_access_group) {
+      $entities = $webform_access_group->getEntityIds();
+      foreach ($entities as $entity) {
+        list($source_entity_type, $source_entity_id) = explode(':', $entity);
+        if (!$entity_type || $source_entity_type === $entity_type) {
+          $source_entity_ids[] = $source_entity_id;
+        }
+      }
+    }
+    return $this->entityTypeManager->getStorage($entity_type)->loadMultiple($source_entity_ids);
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessGroupStorageInterface.php b/web/modules/webform/modules/webform_access/src/WebformAccessGroupStorageInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..874af230ae6c1a7e9ad4334e845e74218439a470
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessGroupStorageInterface.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
+use Drupal\Core\Config\Entity\ImportableEntityStorageInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\webform\WebformInterface;
+
+/**
+ * Provides an interface for Webform Access Group storage.
+ */
+interface WebformAccessGroupStorageInterface extends ConfigEntityStorageInterface, ImportableEntityStorageInterface {
+
+  /**
+   * Load webform access groups by their related entity references.
+   *
+   * @param \Drupal\webform\WebformInterface|null $webform
+   *   (optional) The webform that the submission token is associated with.
+   * @param \Drupal\Core\Entity\EntityInterface|null $source_entity
+   *   (optional) A webform submission source entity.
+   * @param \Drupal\Core\Session\AccountInterface|null $account
+   *   (optional) A user account.
+   * @param string $type
+   *   (optional) Webform access type.
+   *
+   * @return \Drupal\webform\WebformSubmissionInterface[]
+   *   An array of webform access group objects indexed by their ids.
+   */
+  public function loadByEntities(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $type = NULL);
+
+  /**
+   * Get source entities associated with a user account.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   A user account.
+   * @param string|null $entity_type
+   *   Source entity type.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface[]
+   *   Get source entities associated with a user account.
+   */
+  public function getUserEntities(AccountInterface $account, $entity_type = NULL);
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessTypeAccessControlHandler.php b/web/modules/webform/modules/webform_access/src/WebformAccessTypeAccessControlHandler.php
new file mode 100644
index 0000000000000000000000000000000000000000..4a2b46883151b5dbd410965c0f7978065d4421d8
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessTypeAccessControlHandler.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the access control handler for the webform access entity type.
+ *
+ * @see \Drupal\webform_access\Entity\WebformAccessType.
+ */
+class WebformAccessTypeAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    return AccessResult::allowedIfHasPermission($account, 'administer webform');
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessTypeDeleteForm.php b/web/modules/webform/modules/webform_access/src/WebformAccessTypeDeleteForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..f11367e500ec36baf286d62e5ce13ecba9517dbd
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessTypeDeleteForm.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\webform\Form\WebformConfigEntityDeleteFormBase;
+
+/**
+ * Provides a delete webform access type form.
+ */
+class WebformAccessTypeDeleteForm extends WebformConfigEntityDeleteFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $confirmCheckbox = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return [
+      'title' => [
+        '#markup' => $this->t('This action will…'),
+      ],
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => [
+          $this->t('Remove configuration'),
+          $this->t('Affect any access groups which use this type'),
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessTypeForm.php b/web/modules/webform/modules/webform_access/src/WebformAccessTypeForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..77e4991f9a8a66c9ec2acce1aa7cf6782dcab883
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessTypeForm.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Utility\WebformDialogHelper;
+
+/**
+ * Provides a form to define a webform access type.
+ */
+class WebformAccessTypeForm extends EntityForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\webform_access\WebformAccessTypeInterface $webform_access_type */
+    $webform_access_type = $this->entity;
+
+    $form['label'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Label'),
+      '#maxlength' => 255,
+      '#required' => TRUE,
+      '#attributes' => ($webform_access_type->isNew()) ? ['autofocus' => 'autofocus'] : [],
+      '#default_value' => $webform_access_type->label(),
+    ];
+    $form['id'] = [
+      '#type' => 'machine_name',
+      '#machine_name' => [
+        'exists' => '\Drupal\webform_access\Entity\WebformAccessType::load',
+        'label' => '<br/>' . $this->t('Machine name'),
+      ],
+      '#maxlength' => 32,
+      '#field_suffix' => ' (' . $this->t('Maximum @max characters', ['@max' => 32]) . ')',
+      '#required' => TRUE,
+      '#disabled' => !$webform_access_type->isNew(),
+      '#default_value' => $webform_access_type->id(),
+    ];
+
+    return parent::form($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, FormStateInterface $form_state) {
+    $actions = parent::actions($form, $form_state);
+
+    // Open delete button in a modal dialog.
+    if (isset($actions['delete'])) {
+      $actions['delete']['#attributes'] = WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW, $actions['delete']['#attributes']['class']);
+      WebformDialogHelper::attachLibraries($actions['delete']);
+    }
+
+    return $actions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\webform_access\WebformAccessTypeInterface $webform_access_type */
+    $webform_access_type = $this->getEntity();
+    $webform_access_type->save();
+
+    $context = [
+      '@label' => $webform_access_type->label(),
+      'link' => $webform_access_type->toLink($this->t('Edit'), 'edit-form')->toString(),
+    ];
+    $this->logger('webform')->notice('Access type @label saved.', $context);
+
+    $this->messenger()->addStatus($this->t('Access type %label saved.', ['%label' => $webform_access_type->label()]));
+
+    $form_state->setRedirect('entity.webform_access_type.collection');
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessTypeInterface.php b/web/modules/webform/modules/webform_access/src/WebformAccessTypeInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..7ba2c0e0398012059400ec0cad8c2c34ce76b1d6
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessTypeInterface.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+
+/**
+ * Provides an interface defining a webform access type entity.
+ */
+interface WebformAccessTypeInterface extends ConfigEntityInterface {
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessTypeListBuilder.php b/web/modules/webform/modules/webform_access/src/WebformAccessTypeListBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..03d58c6955f51fe3db577764f862ffc35890cf80
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessTypeListBuilder.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\webform\Utility\WebformDialogHelper;
+use Drupal\webform_access\Entity\WebformAccessGroup;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a class to build a listing of webform access type entities.
+ *
+ * @see \Drupal\webform\Entity\WebformOption
+ */
+class WebformAccessTypeListBuilder extends ConfigEntityListBuilder {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $limit = FALSE;
+
+  /**
+   * Access group storage.
+   *
+   * @var \Drupal\webform_access\WebformAccessGroupStorageInterface
+   */
+  protected $accessGroupStorage;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, WebformAccessGroupStorageInterface $access_group_storage) {
+    parent::__construct($entity_type, $storage);
+    $this->accessGroupStorage = $access_group_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('entity.manager')->getStorage($entity_type->id()),
+      $container->get('entity.manager')->getStorage('webform_access_group')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    $build = [];
+
+    // Display info.
+    $build['info'] = $this->buildInfo();
+
+    // Table.
+    $build += parent::render();
+    $build['table']['#sticky'] = TRUE;
+
+    // Attachments.
+    $build['#attached']['library'][] = 'webform/webform.admin.dialog';
+
+    return $build;
+  }
+
+  /**
+   * Build information summary.
+   *
+   * @return array
+   *   A render array representing the information summary.
+   */
+  protected function buildInfo() {
+    $total = $this->getStorage()->getQuery()->count()->execute();
+    if (!$total) {
+      return [];
+    }
+
+    return [
+      '#markup' => $this->formatPlural($total, '@total access type', '@total access types', ['@total' => $total]),
+      '#prefix' => '<div>',
+      '#suffix' => '</div>',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header['label'] = $this->t('Label');
+    $header['groups'] = [
+      'data' => $this->t('Groups'),
+      'class' => [RESPONSIVE_PRIORITY_LOW],
+    ];
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    /** @var \Drupal\webform_access\WebformAccessTypeInterface $entity */
+
+    // Label.
+    $row['label'] = $entity->toLink($entity->label(), 'edit-form');
+
+    // Groups.
+    $entity_ids = $this->accessGroupStorage->getQuery()
+      ->condition('type', $entity->id())
+      ->execute();
+    $items = [];
+    if ($entity_ids) {
+      $webform_access_groups = WebformAccessGroup::loadMultiple($entity_ids);
+      foreach ($webform_access_groups as $webform_access_group) {
+        $items[] = $webform_access_group->label();
+      }
+    }
+    $row['groups'] = ['data' => ['#theme' => 'item_list', '#items' => $items]];
+    return $row + parent::buildRow($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOperations(EntityInterface $entity, $type = 'edit') {
+    $operations = parent::getDefaultOperations($entity);
+    if (isset($operations['delete'])) {
+      $operations['delete']['attributes'] = WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW);
+    }
+    return $operations;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOperations(EntityInterface $entity) {
+    return parent::buildOperations($entity) + [
+      '#prefix' => '<div class="webform-dropbutton">',
+      '#suffix' => '</div>',
+    ];
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessTypeStorage.php b/web/modules/webform/modules/webform_access/src/WebformAccessTypeStorage.php
new file mode 100644
index 0000000000000000000000000000000000000000..7080efff97aa42932b1237025e300f0ef5afab01
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessTypeStorage.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\Core\Config\Entity\ConfigEntityStorage;
+
+/**
+ * Storage controller class for "webform_access_type" configuration entities.
+ */
+class WebformAccessTypeStorage extends ConfigEntityStorage implements WebformAccessTypeStorageInterface {
+
+}
diff --git a/web/modules/webform/modules/webform_access/src/WebformAccessTypeStorageInterface.php b/web/modules/webform/modules/webform_access/src/WebformAccessTypeStorageInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..82c79e3cae869152cc187579376e3f80532c1f96
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/src/WebformAccessTypeStorageInterface.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Drupal\webform_access;
+
+use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
+use Drupal\Core\Config\Entity\ImportableEntityStorageInterface;
+
+/**
+ * Provides an interface for Webform Access Group storage.
+ */
+interface WebformAccessTypeStorageInterface extends ConfigEntityStorageInterface, ImportableEntityStorageInterface {
+
+}
diff --git a/web/modules/webform/modules/webform_access/webform_access.info.yml b/web/modules/webform/modules/webform_access/webform_access.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..35b8a7749f9f5c66fcdba1b0e220ec41971fffdc
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/webform_access.info.yml
@@ -0,0 +1,14 @@
+name: 'Webform Access'
+description: 'Provides webform access controls for webform nodes.'
+# core: 8.x
+package: Webform
+type: module
+dependencies:
+  - 'drupal:webform'
+  - 'drupal:webform_node'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_access/webform_access.install b/web/modules/webform/modules/webform_access/webform_access.install
new file mode 100644
index 0000000000000000000000000000000000000000..a2267a8b0acabef483198e835e681c37e1c6c3e9
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/webform_access.install
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Webform access module.
+ */
+
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Implements hook_schema().
+ */
+function webform_access_schema() {
+  $schema['webform_access_group_user'] = [
+    'description' => 'Stores users associated with a webform access group.',
+    'fields' => [
+      'group_id' => [
+        'description' => 'The webform access group id.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ],
+      'uid' => [
+        'description' => 'The user id.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ],
+    ],
+    'indexes' => [
+      'indexing' => ['group_id', 'uid'],
+    ],
+    'primary key' => ['group_id', 'uid'],
+  ];
+
+  $schema['webform_access_group_entity'] = [
+    'description' => 'Stores users associated with a webform access group.',
+    'fields' => [
+      'group_id' => [
+        'description' => 'The webform access group id.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ],
+      'entity_type' => [
+        'description' => 'The source entity type.',
+        'type' => 'varchar',
+        'length' => EntityTypeInterface::ID_MAX_LENGTH,
+        'not null' => TRUE,
+      ],
+      'entity_id' => [
+        'description' => 'The source entity id.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ],
+      'field_name' => [
+        'description' => 'The webform field name.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ],
+      'webform_id' => [
+        'description' => 'The webform id.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ],
+    ],
+    'indexes' => [
+      'source_entity' => ['entity_type', 'entity_id', 'field_name', 'webform_id'],
+    ],
+    'primary key' => ['group_id', 'webform_id', 'entity_type', 'entity_id', 'field_name'],
+  ];
+
+  return $schema;
+}
+
+/**
+ * Issue #3034127: Error webform access with postgres.
+ */
+function webform_access_update_8001() {
+  // Converts storage for 'webform_access_group_entity.entity_id'
+  // from integer to string.
+  \Drupal::database()
+    ->schema()
+    ->changeField('webform_access_group_entity', 'entity_id', 'entity_id', [
+      'description' => 'The source entity id.',
+      'type' => 'varchar',
+      'length' => 32,
+      'not null' => TRUE,
+    ]);
+}
diff --git a/web/modules/webform/modules/webform_access/webform_access.libraries.yml b/web/modules/webform/modules/webform_access/webform_access.libraries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c13c9bdf59c6c69cf547042fc83a1d98181ea722
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/webform_access.libraries.yml
@@ -0,0 +1,8 @@
+webform_access.admin:
+  version: VERSION
+  js:
+    js/webform_access.admin.js: {}
+  dependencies:
+    - core/drupal
+    - core/jquery
+    - core/jquery.once
diff --git a/web/modules/webform/modules/webform_access/webform_access.links.action.yml b/web/modules/webform/modules/webform_access/webform_access.links.action.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2e98f69c2b2aaea5d1c29bebb6db5a3519f6fd6f
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/webform_access.links.action.yml
@@ -0,0 +1,11 @@
+entity.webform_access_group.add_form:
+  route_name: entity.webform_access_group.add_form
+  title: 'Add access group'
+  appears_on:
+    - entity.webform_access_group.collection
+
+entity.webform_access_type.add_form:
+  route_name: entity.webform_access_type.add_form
+  title: 'Add access type'
+  appears_on:
+    - entity.webform_access_type.collection
diff --git a/web/modules/webform/modules/webform_access/webform_access.links.task.yml b/web/modules/webform/modules/webform_access/webform_access.links.task.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2329f888df45a191647d2cd86afa388012f6893f
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/webform_access.links.task.yml
@@ -0,0 +1,29 @@
+webform.config.webform_access:
+  title: 'Access'
+  route_name: entity.webform_access_group.collection
+  base_route: entity.webform.collection
+  weight: 40
+
+webform.config.webform_access_group:
+  title: 'Groups'
+  route_name: entity.webform_access_group.collection
+  parent_id: webform.config.webform_access
+  weight: 10
+
+webform.config.webform_access_type:
+  title: 'Types'
+  route_name: entity.webform_access_type.collection
+  parent_id: webform.config.webform_access
+  weight: 20
+
+entity.webform_access_group.edit_form:
+  route_name: entity.webform_access_group.edit_form
+  base_route: entity.webform_access_group.edit_form
+  title: 'Edit'
+  weight: 0
+
+entity.webform_access_type.edit_form:
+  route_name: entity.webform_access_type.edit_form
+  base_route: entity.webform_access_type.edit_form
+  title: 'Edit'
+  weight: 0
diff --git a/web/modules/webform/modules/webform_access/webform_access.module b/web/modules/webform/modules/webform_access/webform_access.module
new file mode 100644
index 0000000000000000000000000000000000000000..f3d122a5e4762aae6a2935d0eabb6c525e7957eb
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/webform_access.module
@@ -0,0 +1,426 @@
+<?php
+
+/**
+ * @file
+ * Provides webform access controls for webform nodes.
+ */
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\webform\Utility\WebformElementHelper;
+use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformSubmissionInterface;
+use Drupal\webform_access\Entity\WebformAccessGroup;
+
+/**
+ * Implements hook_webform_help_info().
+ */
+function webform_access_webform_help_info() {
+  $help = [];
+
+  // Access group.
+  $help['webform_access_group'] = [
+    'group' => 'access',
+    'title' => t('Webform Access: Group'),
+    'content' => t('The <strong>Access group</strong> page lists reusable groups used to access webform source entity and users.'),
+    'video_id' => 'access',
+    'routes' => [
+      // @see /admin/structure/webform/access/group/manage
+      'entity.webform_access_group.collection',
+    ],
+  ];
+
+  // Access type.
+  $help['webform_access_type'] = [
+    'type' => 'access',
+    'title' => t('Webform Access: Type'),
+    'content' => t('The <strong>Access type</strong> page lists types of groups used to send email notifications to users.'),
+    'video_id' => 'access',
+    'routes' => [
+      // @see /admin/structure/webform/access/type/manage
+      'entity.webform_access_type.collection',
+    ],
+  ];
+
+  return $help;
+}
+
+/******************************************************************************/
+// Delete relationship hooks.
+/******************************************************************************/
+
+/**
+ * Implements hook_user_delete().
+ */
+function webform_access_user_delete(EntityInterface $entity) {
+  \Drupal::database()->delete('webform_access_group_user')
+    ->condition('uid', $entity->id())
+    ->execute();
+}
+
+/**
+ * Implements hook_node_delete().
+ */
+function webform_access_node_delete(EntityInterface $entity) {
+  \Drupal::database()->delete('webform_access_group_entity')
+    ->condition('entity_type', 'node')
+    ->condition('entity_id', $entity->id())
+    ->execute();
+}
+
+/**
+ * Implements hook_field_config_delete().
+ */
+function webform_access_field_config_delete(EntityInterface $entity) {
+  /** @var Drupal\field\Entity\FieldConfig $definition */
+  if ($entity->getType() === 'webform' && $entity->getEntityTypeId() === 'node') {
+    $entity_ids = \Drupal::entityQuery('webform_access_group')
+      ->condition('type', $entity->getTargetBundle())
+      ->execute();
+    if ($entity_ids) {
+      \Drupal::database()->delete('webform_access_group_entity')
+        ->condition('entity_type', 'node')
+        ->condition('entity_id', $entity_ids, 'IN')
+        ->condition('field_name', $entity->getName())
+        ->execute();
+    }
+  }
+}
+
+/**
+ * Implements hook_field_storage_config_delete().
+ */
+function webform_access_field_storage_config_delete(EntityInterface $entity) {
+  /** @var Drupal\field\Entity\FieldStorageConfig $entity */
+  if ($entity->getType() === 'webform') {
+    \Drupal::database()->delete('webform_access_group_entity')
+      ->condition('entity_type', $entity->getEntityTypeId())
+      ->condition('field_name', $entity->getName())
+      ->execute();
+  }
+}
+
+/******************************************************************************/
+// Access checking.
+/******************************************************************************/
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ *
+ * Add webform access group to local task cacheability.
+ *
+ * @see \Drupal\Core\Menu\Plugin\Block\LocalTasksBlock::build
+ */
+function webform_access_menu_local_tasks_alter(&$data, $route_name) {
+  // Change config entities 'Translate *' tab to be just label 'Translate'.
+  $webform_entities = [
+    'webform_access_group',
+    'webform_access_type',
+  ];
+  foreach ($webform_entities as $webform_entity) {
+    if (isset($data['tabs'][0]["config_translation.local_tasks:entity.$webform_entity.config_translation_overview"]['#link']['title'])) {
+      $data['tabs'][0]["config_translation.local_tasks:entity.$webform_entity.config_translation_overview"]['#link']['title'] = t('Translate');
+    }
+  }
+
+  $route_name = \Drupal::routeMatch()->getRouteName();
+  if ($route_name !== 'entity.node.canonical' && strpos($route_name, 'entity.node.webform.') !== 0) {
+    return;
+  }
+
+  /** @var \Drupal\webform\WebformRequestInterface $request_handler */
+  $request_handler = \Drupal::service('webform.request');
+  $account = \Drupal::currentUser();
+  $webform = $request_handler->getCurrentWebform();
+  $source_entity = $request_handler->getCurrentSourceEntity();
+  if (!$webform || $source_entity) {
+    return;
+  }
+
+  /** @var \Drupal\webform_access\WebformAccessGroupStorageInterface $webform_access_group */
+  $webform_access_group_storage = \Drupal::entityTypeManager()->getStorage('webform_access_group');
+  $webform_access_groups = $webform_access_group_storage->loadByEntities($webform, $source_entity, $account);
+  if (empty($webform_access_groups)) {
+    return;
+  }
+
+  /** @var \Drupal\Core\Cache\CacheableMetadata $cacheability */
+  $cacheability = $data['cacheability'];
+  foreach ($webform_access_groups as $webforn_access_group) {
+    $cacheability->addCacheableDependency($webforn_access_group);
+  }
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_access().
+ */
+function webform_access_webform_access(WebformInterface $webform, $operation, AccountInterface $account) {
+  // Prevent recursion when a webform is being passed as the source entity
+  // via the URL.
+  // @see \Drupal\webform\Plugin\WebformSourceEntity\QueryStringWebformSourceEntity::getSourceEntity
+  if (\Drupal::request()->query->get('source_entity_type') === 'webform') {
+    return AccessResult::neutral();
+  }
+
+  /** @var \Drupal\webform\WebformRequestInterface $request_handler */
+  $request_handler = \Drupal::service('webform.request');
+  $source_entity = $request_handler->getCurrentSourceEntity();
+  if (!$source_entity) {
+    return AccessResult::neutral();
+  }
+
+  /** @var \Drupal\webform_access\WebformAccessGroupStorageInterface $webform_access_group */
+  $webform_access_group_storage = \Drupal::entityTypeManager()->getStorage('webform_access_group');
+  $webform_access_groups = $webform_access_group_storage->loadByEntities($webform, $source_entity, $account);
+  if (empty($webform_access_groups)) {
+    return AccessResult::neutral();
+  }
+
+  $permission = str_replace('submission_', '', $operation);
+  foreach ($webform_access_groups as $webforn_access_group) {
+    $permissions = $webforn_access_group->get('permissions');
+    if (
+      // Is admin.
+      in_array('administer', $permissions)
+      // Is operation any.
+      || in_array($permission, $permissions)) {
+      return AccessResult::allowed()
+        ->cachePerUser()
+        ->addCacheableDependency($webforn_access_group);
+    }
+  }
+
+  // No opinion.
+  return AccessResult::neutral();
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_access().
+ */
+function webform_access_webform_submission_access(WebformSubmissionInterface $webform_submission, $operation, AccountInterface $account) {
+  if (!in_array($operation, ['view', 'update', 'delete'])) {
+    return AccessResult::neutral();
+  }
+
+  $webform = $webform_submission->getWebform();
+  $source_entity = $webform_submission->getSourceEntity();
+  if (!$source_entity) {
+    return AccessResult::neutral();
+  }
+
+  /** @var \Drupal\webform_access\WebformAccessGroupStorageInterface $webform_access_group */
+  $webform_access_group_storage = \Drupal::entityTypeManager()->getStorage('webform_access_group');
+  $webform_access_groups = $webform_access_group_storage->loadByEntities($webform, $source_entity, $account);
+  if (empty($webform_access_groups)) {
+    return AccessResult::neutral();
+  }
+
+  foreach ($webform_access_groups as $webforn_access_group) {
+    $permissions = $webforn_access_group->get('permissions');
+    if (
+      // Is admin.
+      (in_array('administer', $permissions)) ||
+      // Is operation any.
+      (in_array($operation . '_any', $permissions)) ||
+      // Is operation own.
+      (in_array($operation . '_own', $permissions) && $webform_submission->getOwnerId() == $account->id())
+    ) {
+      return AccessResult::allowed()
+        ->cachePerUser()
+        ->addCacheableDependency($webforn_access_group);
+    }
+  }
+
+  // No opinion.
+  return AccessResult::neutral();
+}
+
+/******************************************************************************/
+// Webform access groups (node) entity.
+/******************************************************************************/
+
+/**
+ * Implements hook_field_widget_form_alter().
+ */
+function webform_access_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) {
+  /** @var \Drupal\Core\Field\FieldItemListInterface $items */
+  $items = $context['items'];
+  $field_definition = $items->getFieldDefinition();
+  if ($field_definition->getType() !== 'webform') {
+    return;
+  }
+  if ($items->getEntity()->getEntityTypeId() !== 'node') {
+    return;
+  }
+
+  $node = $items->getEntity();
+
+  $default_value = ($node->id()) ? \Drupal::database()->select('webform_access_group_entity', 'ge')
+    ->fields('ge', ['group_id'])
+    ->condition('entity_type', 'node')
+    ->condition('entity_id', $node->id())
+    ->condition('webform_id', $element['target_id']['#default_value'])
+    ->condition('field_name', $field_definition->getName())
+    ->execute()->fetchCol() : [];
+
+  $element['settings']['webform_access_group'] = _webform_access_group_build_element(
+    $default_value,
+    [],
+    $form_state
+  );
+}
+
+/**
+ * Implements hook_form_BASE_FORM_ID_alter().
+ */
+function webform_access_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  /** @var \Drupal\webform\WebformEntityReferenceManagerInterface $entity_reference_manager */
+  $entity_reference_manager = \Drupal::service('webform.entity_reference_manager');
+  $node = $form_state->getFormObject()->getEntity();
+  $field_names = $entity_reference_manager->getFieldNames($node);
+  if ($field_names) {
+    $form['actions']['submit']['#submit'][] = '_webform_access_form_node_form_submit';
+  }
+}
+
+/**
+ * Webform access group submit handler.
+ */
+function _webform_access_form_node_form_submit(&$form, FormStateInterface $form_state) {
+  /** @var \Drupal\webform\WebformEntityReferenceManagerInterface $entity_reference_manager */
+  $entity_reference_manager = \Drupal::service('webform.entity_reference_manager');
+  $node = $form_state->getFormObject()->getEntity();
+  $field_names = $entity_reference_manager->getFieldNames($node);
+
+  $record = [
+    'entity_type' => 'node',
+    'entity_id' => $node->id(),
+  ];
+  foreach ($field_names as $field_name) {
+    $value = $form_state->getValue($field_name);
+    // Handle hidden webform fields.
+    if ($value === NULL) {
+      continue;
+    }
+
+    $record['field_name'] = $field_name;
+    // Delete all existing records.
+    \Drupal::database()->delete('webform_access_group_entity')
+      ->condition('entity_type', $record['entity_type'])
+      ->condition('entity_id', $record['entity_id'])
+      ->condition('field_name', $record['field_name'])
+      ->execute();
+    foreach ($value as $item) {
+      $record['webform_id'] = $item['target_id'];
+      foreach ($item['settings']['webform_access_group'] as $group_id) {
+        $record['group_id'] = $group_id;
+        // Insert new record.
+        \Drupal::database()->insert('webform_access_group_entity')
+          ->fields(['group_id', 'entity_type', 'entity_id', 'field_name', 'webform_id'])
+          ->values($record)
+          ->execute();
+        // Invalidate cache tags.
+        WebformAccessGroup::load($group_id)->invalidateTags();
+      }
+    }
+  }
+}
+
+/******************************************************************************/
+// Webform access group users.
+/******************************************************************************/
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Add the webform access group to an individual user's account page.
+ */
+function webform_access_form_user_form_alter(&$form, FormStateInterface $form_state) {
+  // Make sure some webform access groups exist before displaying
+  // the webform access details widget.
+  if (!WebformAccessGroup::loadMultiple()) {
+    return;
+  }
+
+  // Only display the webform access detail widget if the current user can
+  // administer webform and users.
+  if (!\Drupal::currentUser()->hasPermission('administer webform')
+    || !\Drupal::currentUser()->hasPermission('administer users')) {
+    return;
+  }
+
+  $account = $form_state->getFormObject()->getEntity();
+  $default_value = \Drupal::database()->select('webform_access_group_user', 'gu')
+    ->fields('gu', ['group_id'])
+    ->condition('uid', $account->id())
+    ->execute()->fetchCol();
+
+  $form['webform_access'] = [
+    '#type' => 'details',
+    '#title' => t('Webform access'),
+    '#open' => TRUE,
+    '#weight' => 5,
+  ];
+  $form['webform_access']['webform_access_group'] = _webform_access_group_build_element(
+    $default_value,
+    $form,
+    $form_state
+  );
+
+  $form['actions']['submit']['#submit'][] = '_webform_access_user_profile_form_submit';
+}
+
+/**
+ * Submit callback for the user profile form to save the webform_access user setting.
+ */
+function _webform_access_user_profile_form_submit($form, FormStateInterface $form_state) {
+  $account = $form_state->getFormObject()->getEntity();
+  // Delete all existing records.
+  \Drupal::database()->delete('webform_access_group_user')
+    ->condition('uid', $account->id())
+    ->execute();
+  $record = ['uid' => $account->id()];
+  $value = $form_state->getValue('webform_access_group');
+  foreach ($value as $group_id) {
+    $record['group_id'] = $group_id;
+    // Insert new record.
+    \Drupal::database()->insert('webform_access_group_user')
+      ->fields(['group_id', 'uid'])
+      ->values($record)
+      ->execute();
+    WebformAccessGroup::load($group_id)->invalidateTags();
+  }
+}
+
+/******************************************************************************/
+// Webform access group helper functions.
+/******************************************************************************/
+
+/**
+ * Build element used to select webform access groups.
+ *
+ * @param array $default_value
+ *   Array of default group ids.
+ * @param array $form
+ *   An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ *   The current state of the form.
+ *
+ * @return array
+ *   Element used to select webform access groups.
+ */
+function _webform_access_group_build_element(array $default_value, array $form, FormStateInterface $form_state) {
+  $element = [
+    '#type' => 'webform_entity_select',
+    '#title' => 'Access group',
+    '#target_type' => 'webform_access_group',
+    '#selection_handler' => 'default:webform_access_group',
+    '#multiple' => TRUE,
+    '#select2' => TRUE,
+    '#default_value' => $default_value,
+    '#access' => \Drupal::currentUser()->hasPermission('administer webform'),
+  ];
+  return WebformElementHelper::process($element);
+}
diff --git a/web/modules/webform/modules/webform_access/webform_access.routing.yml b/web/modules/webform/modules/webform_access/webform_access.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..50027dddd77cf7641885ca450d5e9c0758f08bf4
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/webform_access.routing.yml
@@ -0,0 +1,75 @@
+# Access group.
+
+entity.webform_access_group.collection:
+  path: '/admin/structure/webform/access/group/manage'
+  defaults:
+    _entity_list: 'webform_access_group'
+    _title: 'Webforms: Access Group'
+  requirements:
+    _permission: 'administer webform'
+
+entity.webform_access_group.add_form:
+  path: '/admin/structure/webform/access/group/manage/add'
+  defaults:
+    _entity_form: 'webform_access_group.add'
+    _title: 'Add webform access group'
+  requirements:
+    _entity_create_access: 'webform_access_group'
+
+entity.webform_access_group.edit_form:
+  path: '/admin/structure/webform/access/group/manage/{webform_access_group}'
+  defaults:
+    _entity_form: 'webform_access_group.edit'
+    _title: 'Edit webform access group'
+  requirements:
+    _entity_access: 'webform_access_group.update'
+
+entity.webform_access_group.duplicate_form:
+  path: '/admin/structure/webform/access/group/{webform_access_group}/duplicate'
+  defaults:
+    _entity_form: 'webform_access_group.duplicate'
+    _title: 'Duplicate webform access group'
+  requirements:
+    _entity_access: 'webform_access_group.duplicate'
+
+entity.webform_access_group.delete_form:
+  path: '/admin/structure/webform/access/group/{webform_access_group}/delete'
+  defaults:
+    _entity_form: 'webform_access_group.delete'
+    _title: 'Delete webform access group'
+  requirements:
+    _entity_access: 'webform_access_group.delete'
+
+# Access type.
+
+entity.webform_access_type.collection:
+  path: '/admin/structure/webform/access/type/manage'
+  defaults:
+    _entity_list: 'webform_access_type'
+    _title: 'Webforms: Access Group'
+  requirements:
+    _permission: 'administer webform'
+
+entity.webform_access_type.add_form:
+  path: '/admin/structure/webform/access/type/manage/add'
+  defaults:
+    _entity_form: 'webform_access_type.add'
+    _title: 'Add webform access type'
+  requirements:
+    _entity_create_access: 'webform_access_type'
+
+entity.webform_access_type.edit_form:
+  path: '/admin/structure/webform/access/type/manage/{webform_access_type}'
+  defaults:
+    _entity_form: 'webform_access_type.edit'
+    _title: 'Edit webform access type'
+  requirements:
+    _entity_access: 'webform_access_type.update'
+
+entity.webform_access_type.delete_form:
+  path: '/admin/structure/webform/access/type/{webform_access_type}/delete'
+  defaults:
+    _entity_form: 'webform_access_type.delete'
+    _title: 'Delete webform access type'
+  requirements:
+    _entity_access: 'webform_access_type.delete'
diff --git a/web/modules/webform/modules/webform_access/webform_access.services.yml b/web/modules/webform/modules/webform_access/webform_access.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fcabf34ff88eea845430beba0d782666ada0090a
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/webform_access.services.yml
@@ -0,0 +1,6 @@
+services:
+  webform_access.breadcrumb:
+    class: Drupal\webform_access\Breadcrumb\WebformAccessBreadcrumbBuilder
+    arguments: ['@string_translation']
+    tags:
+      - { name: breadcrumb_builder, priority: 1003 }
diff --git a/web/modules/webform/modules/webform_access/webform_access.tokens.inc b/web/modules/webform/modules/webform_access/webform_access.tokens.inc
new file mode 100644
index 0000000000000000000000000000000000000000..be5fc5aa40a2ad811a4e5a6a86741830dcd5313c
--- /dev/null
+++ b/web/modules/webform/modules/webform_access/webform_access.tokens.inc
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for webform access type.
+ */
+
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\webform_access\Entity\WebformAccessType;
+
+/**
+ * Implements hook_token_info().
+ */
+function webform_access_token_info() {
+  $types = [];
+  $types['webform_access'] = [
+    'name' => t('Webform access'),
+    'description' => t("Tokens related to webform access group types. <em>This token is only available to a Webform email handler's 'To', 'CC', and 'BCC' email recipents.</em>"),
+    'needs-data' => 'webform_access',
+  ];
+
+  $tokens = [];
+  $webform_access = [];
+  $webform_access_types = WebformAccessType::loadMultiple();
+  $webform_access['type'] = [
+    'name' => t('All users'),
+    'description' => t('The email addresses of all users assigned to the current webform.'),
+  ];
+  foreach ($webform_access_types as $webform_access_type_name => $webform_access_type) {
+    $webform_access['type:' . $webform_access_type_name] = [
+      'name' => $webform_access_type->label(),
+      'description' => t('The email addresses of all webform users assigned to the %title access type for the current webform.', ['%title' => $webform_access_type->label()]),
+    ];
+  }
+  $tokens['webform_access'] = $webform_access;
+
+  /****************************************************************************/
+
+  return ['types' => $types, 'tokens' => $tokens];
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function webform_access_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+  $replacements = [];
+  if ($type == 'webform_access' && !empty($data['webform_access'])) {
+    /** @var \Drupal\webform_access\WebformAccessGroupStorageInterface $webform_access_group_storage */
+    $webform_access_group_storage = \Drupal::entityTypeManager()->getStorage('webform_access_group');
+
+    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submision */
+    $webform_submision = $data['webform_access'];
+    $webform = $webform_submision->getWebform();
+    $source_entity = $webform_submision->getSourceEntity();
+    foreach ($tokens as $name => $original) {
+      $webform_access_type_id = ($name === 'type') ? NULL : str_replace('type:', '', $name);
+
+      /** @var \Drupal\webform_access\WebformAccessGroupStorageInterface $webform_access_group */
+      $webform_access_groups = $webform_access_group_storage->loadByEntities($webform, $source_entity, NULL, $webform_access_type_id);
+
+      if ($webform_access_groups) {
+        $query = \Drupal::database()->select('webform_access_group_user', 'gu');
+        $query->condition('gu.group_id', array_keys($webform_access_groups), 'IN');
+        $query->join('users_field_data', 'u', 'u.uid = gu.uid');
+        $query->fields('u', ['mail']);
+        $query->condition('u.status', 1);
+        $query->condition('u.mail', '', '<>');
+        $query->orderBy('mail');
+        $query->distinct();
+
+        $replacements[$original] = implode(',', $query->execute()->fetchCol());
+      }
+    }
+  }
+
+  return $replacements;
+}
diff --git a/web/modules/webform/modules/webform_attachment/src/Controller/WebformAttachmentController.php b/web/modules/webform/modules/webform_attachment/src/Controller/WebformAttachmentController.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea89ce25d3ac609d5daae8c41e9dd1460c2640fc
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/src/Controller/WebformAttachmentController.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Drupal\webform_attachment\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Render\ElementInfoManagerInterface;
+use Drupal\webform\Plugin\WebformElementManagerInterface;
+use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformSubmissionInterface;
+use Drupal\webform_attachment\Plugin\WebformElement\WebformAttachmentBase;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a controller to return a webform attachment.
+ */
+class WebformAttachmentController extends ControllerBase implements ContainerInjectionInterface {
+
+  /**
+   * Element info.
+   *
+   * @var \Drupal\Core\Render\ElementInfoManager
+   */
+  protected $elementInfo;
+
+  /**
+   * A webform element plugin manager.
+   *
+   * @var \Drupal\webform\Plugin\WebformElementManagerInterface
+   */
+  protected $elementManager;
+
+  /**
+   * Constructs a WebformAttachmentController object.
+   *
+   * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
+   *   The element info manager.
+   * @param \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager
+   *   A webform element plugin manager.
+   */
+  public function __construct(ElementInfoManagerInterface $element_info, WebformElementManagerInterface $element_manager) {
+    $this->elementInfo = $element_info;
+    $this->elementManager = $element_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.element_info'),
+      $container->get('plugin.manager.webform.element')
+    );
+  }
+
+  /**
+   * Response callback to download an attachment.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   * @param string $element
+   *   The attachment element webform key.
+   * @param string $filename
+   *   The attachment filename.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   A response containing the attachment's file.
+   */
+  public function download(WebformInterface $webform, WebformSubmissionInterface $webform_submission, $element, $filename) {
+    // Make sure the webform id and submission webform id match.
+    if ($webform->id() !== $webform_submission->getWebform()->id()) {
+      throw new NotFoundHttpException();
+    }
+
+    // Get the webform element and plugin.
+    $element = $webform_submission->getWebform()->getElement($element) ?: [];
+    $element_plugin = $this->elementManager->getElementInstance($element);
+
+    // Make sure the element is a webform attachment.
+    if (!$element_plugin instanceof WebformAttachmentBase) {
+      throw new NotFoundHttpException();
+    }
+
+    // Make sure element #access is not FALSE.
+    // The #private property is used to to set #access to FALSE.
+    // @see \Drupal\webform\Entity\Webform::initElementsRecursive
+    if (isset($element['#access']) && $element['#access'] === FALSE) {
+      throw new AccessDeniedHttpException();
+    }
+
+    // Make sure the current user can view the element.
+    if (!$element_plugin->checkAccessRules('view', $element)) {
+      throw new AccessDeniedHttpException();
+    }
+
+    /** @var \Drupal\webform_attachment\Element\WebformAttachmentInterface $element_plugin */
+    $element_info = $this->elementInfo->createInstance($element['#type']);
+
+    // Get attachment information.
+    $attachment_name = $element_info::getFileName($element, $webform_submission);
+    $attachment_mime = $element_info::getFileMimeType($element, $webform_submission);
+    $attachment_content = $element_info::getFileContent($element, $webform_submission);
+    $attachment_size = mb_strlen($attachment_content);
+    $attachment_download = (!empty($element['#download'])) ? 'attachment;' : '';
+
+    // Make sure the attachment can be downloaded.
+    if (empty($attachment_name) || empty($attachment_content) || empty($attachment_mime)) {
+      throw new NotFoundHttpException();
+    }
+
+    // Return the file.
+    $headers = [
+      'Content-Length' => $attachment_size,
+      'Content-Type' => $attachment_mime,
+      'Content-Disposition' => $attachment_download . 'filename="' . $filename . '"',
+    ];
+    return new Response($attachment_content, 200, $headers);
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentBase.php b/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..f873d12c60aabd872e1b15e82e9de97c7f121acd
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentBase.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Drupal\webform_attachment\Element;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element\RenderElement;
+use Drupal\Core\Url;
+use Drupal\webform\WebformSubmissionForm;
+use Drupal\webform\WebformSubmissionInterface;
+
+/**
+ * Provides a base class for 'webform_attachment' elements.
+ */
+abstract class WebformAttachmentBase extends RenderElement implements WebformAttachmentInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return [
+      '#filename' => '',
+      '#sanitize' => FALSE,
+      '#link_title' => '',
+      '#download' => FALSE,
+      '#trim' => FALSE,
+      '#process' => [
+        [$class, 'processWebformAttachment'],
+        [$class, 'processAjaxForm'],
+      ],
+      '#theme_wrappers' => ['form_element'],
+    ];
+  }
+
+  /**
+   * Processes a 'webform_attachment' element.
+   */
+  public static function processWebformAttachment(&$element, FormStateInterface $form_state, &$complete_form) {
+    $form_object = $form_state->getFormObject();
+
+    // Attachments only work for webform submissions.
+    if (!$form_object instanceof WebformSubmissionForm) {
+      $element['#access'] = FALSE;
+      return $element;
+    }
+
+    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
+    $webform_submission = $form_object->getEntity();
+
+    // Attachments only work for completed and saved webform submissions.
+    if (!$webform_submission->id() || !$webform_submission->isCompleted()) {
+      $element['#access'] = FALSE;
+      return $element;
+    }
+
+    // Link to file download.
+    $element['link'] = static::getFileLink($element, $webform_submission);
+
+    return $element;
+  }
+
+  /****************************************************************************/
+  // File methods.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getFileName(array $element, WebformSubmissionInterface $webform_submission) {
+    if (isset($element['#filename'])) {
+      /** @var \Drupal\webform\WebformTokenManagerInterface $token_manager */
+      $token_manager = \Drupal::service('webform.token_manager');
+
+      $filename = $token_manager->replace($element['#filename'], $webform_submission);
+
+      // Remove forward slashes from filename to prevent the below error.
+      //
+      //   Parameter "filename" for route
+      //   "entity.webform.user.submission.attachment" must match "[^/]++".
+      $filename = str_replace('/', '', $filename);
+
+      // Sanitize filename.
+      // @see http://stackoverflow.com/questions/2021624/string-sanitizer-for-filename
+      // @see \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::getFileDestinationUri
+      if (!empty($element['#sanitize'])) {
+        /** @var \Drupal\Component\Transliteration\TransliterationInterface $transliteration */
+        $transliteration = \Drupal::service('transliteration');
+        /** @var \Drupal\Core\Language\LanguageManagerInterface $language_manager */
+        $language_manager = \Drupal::service('language_manager');
+        $langcode = $language_manager->getCurrentLanguage()->getId();
+
+        $extension = pathinfo($filename, PATHINFO_EXTENSION);
+
+        $basename = substr(pathinfo($filename, PATHINFO_BASENAME), 0, -strlen(".$extension"));
+        $basename = mb_strtolower($basename);
+        $basename = $transliteration->transliterate($basename, $langcode, '-');
+        $basename = preg_replace('([^\w\s\d\-_~,;:\[\]\(\].]|[\.]{2,})', '', $basename);
+        $basename = preg_replace('/\s+/', '-', $basename);
+        $basename = trim($basename, '-');
+        $filename = $basename . '.' . $extension;
+      }
+
+      return $filename;
+    }
+    else {
+      return $element['#webform_key'] . '.txt';
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getFileMimeType(array $element, WebformSubmissionInterface $webform_submission) {
+    /** @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $file_mime_type_guesser */
+    $file_mime_type_guesser = \Drupal::service('file.mime_type.guesser');
+    $file_name = static::getFileName($element, $webform_submission);
+    return $file_mime_type_guesser->guess($file_name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getFileUrl(array $element, WebformSubmissionInterface $webform_submission) {
+    if (!$webform_submission->id()) {
+      return NULL;
+    }
+
+    $route_name = 'entity.webform.user.submission.attachment';
+    $route_parameters = [
+      'webform' => $webform_submission->getWebform()->id(),
+      'webform_submission' => $webform_submission->id(),
+      'element' => $element['#webform_key'],
+      'filename' => static::getFileName($element, $webform_submission),
+    ];
+    $route_options = ['absolute' => TRUE];
+    $url = Url::fromRoute($route_name, $route_parameters, $route_options);
+    return $url;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getFileLink(array $element, WebformSubmissionInterface $webform_submission) {
+    $title = (!empty($element['#link_title'])) ? $element['#link_title'] : static::getFileName($element, $webform_submission);
+    $url = static::getFileUrl($element, $webform_submission);
+    return [
+      '#type' => 'link',
+      '#title' => $title,
+      '#url' => $url,
+    ];
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentInterface.php b/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..412ce8e7a81152791826c4c1a893dafa993cf31a
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentInterface.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\webform_attachment\Element;
+
+use Drupal\webform\WebformSubmissionInterface;
+
+/**
+ * Provides an interface for webform attachment elements.
+ */
+interface WebformAttachmentInterface {
+
+  /**
+   * Get a webform attachment's file name.
+   *
+   * @param array $element
+   *   The webform attachment element.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   *
+   * @return mixed|string
+   *   The attachment's file name.
+   */
+  public static function getFileName(array $element, WebformSubmissionInterface $webform_submission);
+
+  /**
+   * Get a webform attachment's file content.
+   *
+   * @param array $element
+   *   The webform attachment element.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   *
+   * @return mixed|string
+   *   The attachment's file content.
+   */
+  public static function getFileContent(array $element, WebformSubmissionInterface $webform_submission);
+
+  /**
+   * Get a webform attachment's file type.
+   *
+   * @param array $element
+   *   The webform attachment element.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   *
+   * @return mixed|string
+   *   The attachment's file type.
+   */
+  public static function getFileMimeType(array $element, WebformSubmissionInterface $webform_submission);
+
+  /**
+   * Get a webform attachment's download URL.
+   *
+   * @param array $element
+   *   The webform attachment element.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   *
+   * @return \Drupal\Core\Url|null
+   *   A webform attachment's download URL. Return NULL if the submission is
+   *   not saved to the database.
+   */
+  public static function getFileUrl(array $element, WebformSubmissionInterface $webform_submission);
+
+  /**
+   * Get a webform attachment's file link.
+   *
+   * @param array $element
+   *   The webform attachment element.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   *
+   * @return array
+   *   A renderable array containing a link to the webform attachment's URL.
+   */
+  public static function getFileLink(array $element, WebformSubmissionInterface $webform_submission);
+
+}
diff --git a/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentToken.php b/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentToken.php
new file mode 100644
index 0000000000000000000000000000000000000000..b7dd1d75e57a0523a8956754c9ff9efd53bf1480
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentToken.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\webform_attachment\Element;
+
+use Drupal\webform\WebformSubmissionInterface;
+
+/**
+ * Provides a 'webform_attachment_token' element.
+ *
+ * @FormElement("webform_attachment_token")
+ */
+class WebformAttachmentToken extends WebformAttachmentBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    return parent::getInfo() + [
+      '#template' => '',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getFileContent(array $element, WebformSubmissionInterface $webform_submission) {
+    /** @var \Drupal\webform\WebformTokenManagerInterface $token_manager */
+    $token_manager = \Drupal::service('webform.token_manager');
+    $content = $token_manager->replace($element['#template'], $webform_submission);
+    return (!empty($element['#trim'])) ? trim($content) : $content;
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentTwig.php b/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentTwig.php
new file mode 100644
index 0000000000000000000000000000000000000000..d029af026b8698731fea1f0c9fe70468cfa04a61
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentTwig.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\webform_attachment\Element;
+
+use Drupal\webform\Twig\TwigExtension;
+use Drupal\webform\WebformSubmissionInterface;
+
+/**
+ * Provides a 'webform_attachment_twig' element.
+ *
+ * @FormElement("webform_attachment_twig")
+ */
+class WebformAttachmentTwig extends WebformAttachmentBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    return parent::getInfo() + [
+      '#template' => '',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getFileContent(array $element, WebformSubmissionInterface $webform_submission) {
+    $options = [];
+    $template = $element['#template'];
+    $content = TwigExtension::renderTwigTemplate($webform_submission, $template, $options);
+    return (!empty($element['#trim'])) ? trim($content) : $content;
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentUrl.php b/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentUrl.php
new file mode 100644
index 0000000000000000000000000000000000000000..635cdcc541ad35fe5b880b6288efc92d4f187c6d
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/src/Element/WebformAttachmentUrl.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\webform_attachment\Element;
+
+use Drupal\webform\WebformSubmissionInterface;
+use GuzzleHttp\Exception\RequestException;
+
+/**
+ * Provides a 'webform_attachment_url' element.
+ *
+ * @FormElement("webform_attachment_url")
+ */
+class WebformAttachmentUrl extends WebformAttachmentBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    return parent::getInfo() + [
+      '#url' => '',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getFileContent(array $element, WebformSubmissionInterface $webform_submission) {
+    try {
+      $url = $element['#url'];
+      // URL can contain tokens.
+      /** @var \Drupal\webform\WebformTokenManagerInterface $token_manager */
+      $token_manager = \Drupal::service('webform.token_manager');
+      $url = $token_manager->replace($url, $webform_submission);
+      // Prepend scheme and host to root relative path.
+      if (strpos($url, '/') === 0) {
+        $url = \Drupal::request()->getSchemeAndHttpHost() . $url;
+      }
+      $content = (string) \Drupal::httpClient()->get($url)->getBody();
+    }
+    catch (RequestException $exception) {
+      $content = '';
+    }
+    return (!empty($element['#trim'])) ? trim($content) : $content;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getFileName(array $element, WebformSubmissionInterface $webform_submission) {
+    if (!isset($element['#filename']) && !empty($element['#url'])) {
+      return basename($element['#url']);
+    }
+    else {
+      return parent::getFileName($element, $webform_submission);
+    }
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_attachment/src/Plugin/WebformElement/WebformAttachmentBase.php b/web/modules/webform/modules/webform_attachment/src/Plugin/WebformElement/WebformAttachmentBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..360b96dd9accee04b9080cb6132ab68807d95a54
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/src/Plugin/WebformElement/WebformAttachmentBase.php
@@ -0,0 +1,238 @@
+<?php
+
+namespace Drupal\webform_attachment\Plugin\WebformElement;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Element\WebformMessage;
+use Drupal\webform\Plugin\WebformElement\WebformDisplayOnTrait;
+use Drupal\webform\Plugin\WebformElementAttachmentInterface;
+use Drupal\webform\Plugin\WebformElementBase;
+use Drupal\webform\Plugin\WebformElementDisplayOnInterface;
+use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformSubmissionInterface;
+
+/**
+ * Provides a base class for 'webform_attachment' elements.
+ */
+abstract class WebformAttachmentBase extends WebformElementBase implements WebformElementAttachmentInterface, WebformElementDisplayOnInterface {
+
+  use WebformDisplayOnTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultProperties() {
+    return [
+      // Element settings.
+      'title' => '',
+      // Form display.
+      'title_display' => '',
+      // Display settings.
+      'display_on' => static::DISPLAY_ON_NONE,
+      // Attachment values.
+      'filename' => '',
+      'sanitize' => FALSE,
+      'link_title' => '',
+      'trim' => FALSE,
+      'download' => FALSE,
+      // Attributes.
+      'wrapper_attributes' => [],
+      'label_attributes' => [],
+    ] + $this->getDefaultBaseProperties();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultBaseProperties() {
+    $properties = parent::getDefaultBaseProperties();
+    unset($properties['prepopulate']);
+    unset($properties['states_clear']);
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function formatHtmlItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
+    /** @var \Drupal\webform_attachment\Element\WebformAttachmentInterface $attachment_element */
+    $attachment_element = $this->getFormElementClassDefinition();
+
+    $format = $this->getItemFormat($element);
+    switch ($format) {
+      case 'name';
+        return $attachment_element::getFileName($element, $webform_submission);
+
+      case 'url';
+        return $attachment_element::getFileUrl($element, $webform_submission)->toString();
+
+      default:
+      case 'link':
+        return $attachment_element::getFileLink($element, $webform_submission);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function formatTextItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
+    /** @var \Drupal\webform_attachment\Element\WebformAttachmentInterface $attachment_element */
+    $attachment_element = $this->getFormElementClassDefinition();
+
+    $format = $this->getItemFormat($element);
+    switch ($format) {
+      case 'name';
+        return $attachment_element::getFileName($element, $webform_submission);
+
+      default:
+      case 'link';
+      case 'url';
+        return $attachment_element::getFileUrl($element, $webform_submission)->toString();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasValue(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemFormats() {
+    return [
+      'link' => $this->t('File link'),
+      'name' => $this->t('File name'),
+      'url' => $this->t('File URL'),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemDefaultFormat() {
+    return 'link';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(array &$element, WebformSubmissionInterface $webform_submission) {
+    $key = $element['#webform_key'];
+    $data = $webform_submission->getData();
+    // Make sure attachment element never stores a value.
+    if (isset($data[$key])) {
+      unset($data[$key]);
+      $webform_submission->setData($data);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+    $form['attachment'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Attachment settings'),
+    ];
+    $form['attachment']['message'] = [
+      '#type' => 'webform_message',
+      '#message_message' => $this->t("Please make sure to enable 'Include files as attachments' for each email handler that you want to send this attachment."),
+      '#message_type' => 'warning',
+      '#message_close' => TRUE,
+      '#message_storage' => WebformMessage::STORAGE_SESSION,
+      '#access' => TRUE,
+    ];
+    $form['attachment']['display_on'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Display on'),
+      '#options' => $this->getDisplayOnOptions(TRUE),
+    ];
+    $form['attachment']['display_on_message'] = [
+      '#type' => 'webform_message',
+      '#message_message' => $this->t("The attachment's link will only be diplayed on the form after the submission is completed."),
+      '#message_type' => 'warning',
+      '#access' => TRUE,
+      '#states' => [
+        'visible' => [
+          [':input[name="properties[display_on]"]' => ['value' => 'form']],
+          'or',
+          [':input[name="properties[display_on]"]' => ['value' => 'both']],
+        ],
+      ],
+    ];
+    $form['attachment']['filename'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('File name'),
+      '#description' => $this->t("Please enter the attachment's file name with a file extension. The file extension will be used to determine the attachment's content (mime) type."),
+      '#maxlength' => NULL,
+      '#required' => TRUE,
+    ];
+    $form['attachment']['link_title'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Link title'),
+      '#description' => $this->t('Enter the title to be displayed when the attachment is displayed as a link.'),
+    ];
+    $form['attachment']['trim'] = [
+      '#type' => 'checkbox',
+      '#return_value' => TRUE,
+      '#title' => $this->t("Remove whitespace around the attachment's content"),
+      '#description' => $this->t("If checked, all spaces and returns around the attachment's content with be removed."),
+      '#weight' => 10,
+    ];
+    $form['attachment']['sanitize'] = [
+      '#type' => 'checkbox',
+      '#return_value' => TRUE,
+      '#title' => $this->t('Sanitize file name'),
+      '#description' => $this->t('If checked, file name will be transliterated, lower-cased and all special characters converted to dashes (-).'),
+      '#weight' => 10,
+    ];
+    $form['attachment']['download'] = [
+      '#type' => 'checkbox',
+      '#return_value' => TRUE,
+      '#title' => $this->t('Force users to download the attachment'),
+      '#description' => $this->t('If checked the attachment will be automatically download.'),
+      '#weight' => 10,
+    ];
+    $form['attachment']['tokens'] = ['#access' => TRUE, '#weight' => 10] + $this->tokenManager->buildTreeElement();
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTestValues(array $element, WebformInterface $webform, array $options = []) {
+    // Attachment elements should never get a test value.
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAttachments(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
+    /** @var \Drupal\webform_attachment\Element\WebformAttachmentInterface $attachment_element */
+    $attachment_element = $this->getFormElementClassDefinition();
+
+    $file_content = $attachment_element::getFileContent($element, $webform_submission);
+    $file_name = $attachment_element::getFileName($element, $webform_submission);
+    $file_mime = $attachment_element::getFileMimeType($element, $webform_submission);
+    $file_url = $attachment_element::getFileUrl($element, $webform_submission);
+
+    $attachments = [];
+    if ($file_name && $file_content && $file_mime) {
+      $attachments[] = [
+        'filecontent' => $file_content,
+        'filename' => $file_name,
+        'filemime' => $file_mime,
+        // URI is used when debugging or resending messages.
+        // @see \Drupal\webform\Plugin\WebformHandler\EmailWebformHandler::buildAttachments
+        '_uri' => ($file_url) ? $file_url->toString() : NULL,
+      ];
+    }
+    return $attachments;
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_attachment/src/Plugin/WebformElement/WebformAttachmentToken.php b/web/modules/webform/modules/webform_attachment/src/Plugin/WebformElement/WebformAttachmentToken.php
new file mode 100644
index 0000000000000000000000000000000000000000..11c9ff445003ddb34f3f0f42ecb77d5fc4aeb42a
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/src/Plugin/WebformElement/WebformAttachmentToken.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\webform_attachment\Plugin\WebformElement;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a 'webform_attachment_token' element.
+ *
+ * @WebformElement(
+ *   id = "webform_attachment_token",
+ *   label = @Translation("Attachment token"),
+ *   description = @Translation("Generates an attachment using tokens."),
+ *   category = @Translation("File attachment elements"),
+ * )
+ */
+class WebformAttachmentToken extends WebformAttachmentBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultProperties() {
+    return [
+      'template' => '',
+    ] + parent::getDefaultProperties();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+    $form['attachment']['template'] = [
+      '#type' => 'webform_codemirror',
+      '#mode' => 'text',
+      '#title' => $this->t('Template'),
+    ];
+    return $form;
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_attachment/src/Plugin/WebformElement/WebformAttachmentTwig.php b/web/modules/webform/modules/webform_attachment/src/Plugin/WebformElement/WebformAttachmentTwig.php
new file mode 100644
index 0000000000000000000000000000000000000000..09565027427e5e56022dce87d5ccdf0915221f87
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/src/Plugin/WebformElement/WebformAttachmentTwig.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\webform_attachment\Plugin\WebformElement;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Twig\TwigExtension;
+use Drupal\webform\Utility\WebformElementHelper;
+
+/**
+ * Provides a 'webform_attachment_twig' element.
+ *
+ * @WebformElement(
+ *   id = "webform_attachment_twig",
+ *   label = @Translation("Attachment Twig"),
+ *   description = @Translation("Generates an attachment using Twig."),
+ *   category = @Translation("File attachment elements"),
+ * )
+ */
+class WebformAttachmentTwig extends WebformAttachmentBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultProperties() {
+    return [
+      'template' => '',
+    ] + parent::getDefaultProperties();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+    $form['attachment']['template'] = [
+      '#type' => 'webform_codemirror',
+      '#mode' => 'twig',
+      '#title' => $this->t('Twig'),
+    ];
+    $form['attachment']['help'] = TwigExtension::buildTwigHelp();
+    WebformElementHelper::setPropertyRecursive($form['attachment']['help'], '#access', TRUE);
+
+    return $form;
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_attachment/src/Plugin/WebformElement/WebformAttachmentUrl.php b/web/modules/webform/modules/webform_attachment/src/Plugin/WebformElement/WebformAttachmentUrl.php
new file mode 100644
index 0000000000000000000000000000000000000000..4b393813c28bbb1db9473a696c4d25ba97fa240e
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/src/Plugin/WebformElement/WebformAttachmentUrl.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\webform_attachment\Plugin\WebformElement;
+
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Form\FormStateInterface;
+use GuzzleHttp\Exception\ClientException;
+
+/**
+ * Provides a 'webform_attachment_url' element.
+ *
+ * @WebformElement(
+ *   id = "webform_attachment_url",
+ *   label = @Translation("Attachment URL"),
+ *   description = @Translation("Generates an attachment using a URL."),
+ *   category = @Translation("File attachment elements"),
+ * )
+ */
+class WebformAttachmentUrl extends WebformAttachmentBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultProperties() {
+    return [
+      'url' => '',
+    ] + parent::getDefaultProperties();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+    $form['attachment']['url'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('URL/path'),
+      '#description' => $this->t("Make sure the attachment URL/Path is publicly accessible. The attachment's URL/path will never be displayed to end users."),
+      '#required' => TRUE,
+      '#element_validate' => [[get_class($this), 'validateAttachmentUrl']],
+    ];
+    if (function_exists('imce_process_url_element')) {
+      $url_element = &$form['attachment']['url'];
+      imce_process_url_element($url_element, 'link');
+      $form['#attached']['library'][] = 'webform/imce.input';
+    }
+    return $form;
+  }
+
+  /**
+   * Form API callback. Validate url/path.
+   *
+   * @see \Drupal\Core\Render\Element\Url::validateUrl
+   */
+  public static function validateAttachmentUrl(&$element, FormStateInterface &$form_state) {
+    $value = trim($element['#value']);
+    $form_state->setValueForElement($element, $value);
+
+    // Prepend scheme and host to root relative path.
+    if (strpos($value, '/') === 0) {
+      $value = \Drupal::request()->getSchemeAndHttpHost() . $value;
+    }
+
+    // Validate URL formatting.
+    if ($value !== '' && !UrlHelper::isValid($value, TRUE)) {
+      $form_state->setError($element, t('The URL %url is not valid.', ['%url' => $value]));
+    }
+
+    // Validate URL access.
+    try {
+      \Drupal::httpClient()->head($value);
+    }
+    catch (ClientException $e) {
+      $form_state->setError($element, t('The URL <a href=":url">@url</a> is not available.', [':url' => $value, '@url' => $value]));
+    }
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_attachment/src/Tests/WebformAttachmentTest.php b/web/modules/webform/modules/webform_attachment/src/Tests/WebformAttachmentTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..26271a4d81c47c3c3c3bf19da9f4f427450144b7
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/src/Tests/WebformAttachmentTest.php
@@ -0,0 +1,230 @@
+<?php
+
+namespace Drupal\webform_attachment\Tests;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Entity\WebformSubmission;
+use Drupal\webform\Tests\WebformTestBase;
+use Drupal\webform_attachment\Element\WebformAttachmentToken;
+
+/**
+ * Tests for webform example element.
+ *
+ * @group Webform
+ */
+class WebformAttachmentTest extends WebformTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['token', 'webform_attachment', 'webform_attachment_test'];
+
+  /**
+   * Tests webform attachment.
+   */
+  public function testWebformAttachment() {
+    global $base_url;
+
+    $this->drupalLogin($this->rootUser);
+
+    /**************************************************************************/
+    // Email.
+    /**************************************************************************/
+
+    $webform_id = 'test_attachment_email';
+    $webform_attachment_email = Webform::load('test_attachment_email');
+    $attachment_date = date('Y-m-d');
+
+    // Check that the attachment is added to the sent email.
+    $sid = $this->postSubmission($webform_attachment_email);
+    $sent_email = $this->getLastEmail();
+    $this->assertEqual($sent_email['params']['attachments'][0]['filename'], "attachment_token-$attachment_date.xml", "The attachment's file name");
+    $this->assertEqual($sent_email['params']['attachments'][0]['filemime'], 'application/xml', "The attachment's file mime type");
+    $this->assertEqual($sent_email['params']['attachments'][0]['filecontent'], "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
+<asx:abap xmlns:asx=\"http://www.sap.com/abapxml\" version=\"1.0\">
+   <asx:values>
+      <VERSION>1.0</VERSION>
+      <SENDER>johnsmith@example.com</SENDER>
+      <WEBFORM_ID>test_attachment_email</WEBFORM_ID>
+      <SOURCE>
+         <o2PARAVALU>
+            <NAME>Lastname</NAME>
+            <VALUE>Smith</VALUE>
+         </o2PARAVALU>
+         <o2PARAVALU>
+            <NAME>Firstname</NAME>
+            <VALUE>John</VALUE>
+         </o2PARAVALU>
+         <o2PARAVALU>
+            <NAME>Emailaddress</NAME>
+            <VALUE>johnsmith@example.com</VALUE>
+         </o2PARAVALU>
+      </SOURCE>
+   </asx:values>
+</asx:abap>", "The attachment's file content");
+
+    // Check access to the attachment.
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/attachment_token/attachment_token-$attachment_date.xml");
+    $this->assertResponse(200, 'Access allowed to the attachment');
+
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/attachment_token/any-file-name.text");
+    $this->assertResponse(200, 'Access allowed to the attachment with any file name,');
+
+    $this->drupalGet("/webform/not_a_webform/submissions/$sid/attachment/attachment/any-file-name.text");
+    $this->assertResponse(404, 'Page not found to not a webform');
+
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/email/attachment-$attachment_date.xml");
+    $this->assertResponse(404, 'Page not found when not an attachment element is specified');
+
+    /**************************************************************************/
+    // Token.
+    /**************************************************************************/
+
+    $webform_id = 'test_attachment_token';
+    $webform_attachment_token = Webform::load('test_attachment_token');
+
+    $sid = $this->postSubmissionTest($webform_attachment_token, ['textfield' => 'Some text']);
+
+    // Check that both attachments are displayed on the results page.
+    $this->drupalGet('/admin/structure/webform/manage/test_attachment_token/results/submissions');
+    $this->assertRaw('<td><a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_token/test_token.txt">test_token.txt</a></td>');
+    $this->assertRaw('<td><a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_token_download/test_token.txt">Download</a></td>');
+
+    // Check that only the download attachment is displayed on
+    // the submission page.
+    $this->drupalGet("/admin/structure/webform/manage/test_attachment_token/submission/$sid");
+    $this->assertNoRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_token/test_token.txt">test_token.txt</a>');
+    $this->assertRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_token_download/test_token.txt">Download</a>');
+
+    // Check the attachment's content.
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/webform_attachment_token_download/test_token.txt");
+    $this->assertRaw('textfield: Some text');
+
+    /**************************************************************************/
+    // Twig.
+    /**************************************************************************/
+
+    $webform_id = 'test_attachment_twig';
+    $webform_attachment_twig = Webform::load('test_attachment_twig');
+
+    $sid = $this->postSubmissionTest($webform_attachment_twig, ['textfield' => 'Some text']);
+
+    // Check that both attachments are displayed on the results page.
+    $this->drupalGet('/admin/structure/webform/manage/test_attachment_twig/results/submissions');
+    $this->assertRaw('<td><a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_twig/test_twig.xml">test_twig.xml</a></td>');
+    $this->assertRaw('<td><a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_twig_download/test_twig.xml">Download</a></td>');
+
+    // Check that only the download attachment is displayed on
+    // the submission page.
+    $this->drupalGet("/admin/structure/webform/manage/test_attachment_twig/submission/$sid");
+    $this->assertNoRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_twig/test_twig.txt">test_twig.xml</a>');
+    $this->assertRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_twig_download/test_twig.xml">Download</a>');
+
+    // Check the attachment's content.
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/webform_attachment_twig_download/test_twig.xml");
+    $this->assertRaw('<?xml version="1.0"?>
+<textfield>Some text</textfield>');
+
+    /**************************************************************************/
+    // URL.
+    /**************************************************************************/
+
+    $webform_id = 'test_attachment_url';
+    $webform_attachment_url = Webform::load('test_attachment_url');
+
+    $sid = $this->postSubmissionTest($webform_attachment_url);
+
+    // Check that both attachments are displayed on the results page.
+    $this->drupalGet('/admin/structure/webform/manage/test_attachment_url/results/submissions');
+    $this->assertRaw('<td><a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_url/MAINTAINERS.txt">MAINTAINERS.txt</a></td>');
+    $this->assertRaw('<td><a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_path/durpalicon.png">durpalicon.png</a></td>');
+    $this->assertRaw('<td><a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_url_download/MAINTAINERS.txt">Download</a></td>');
+
+    // Check that only the download attachment is displayed on
+    // the submission page.
+    $this->drupalGet("/admin/structure/webform/manage/test_attachment_url/submission/$sid");
+    $this->assertNoRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_url/MAINTAINERS.txt">MAINTAINERS.txt</a>');
+    $this->assertNoRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_path/durpalicon.png">durpalicon.png</a>');
+    $this->assertRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_url_download/MAINTAINERS.txt">Download</a>');
+
+    $this->assertNoRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_url/MAINTAINERS.txt">MAINTAINERS.txt</a>');
+    $this->assertRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/webform_attachment_url_download/MAINTAINERS.txt">Download</a>');
+
+    // Check the attachment's content.
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/webform_attachment_url_download/MAINTAINERS.txt");
+    $this->assertRaw('https://www.drupal.org/contribute');
+
+    /**************************************************************************/
+    // Access.
+    /**************************************************************************/
+
+    // Switch to anonymous user.
+    $this->drupalLogout();
+
+    $webform_id = 'test_attachment_access';
+    $webform_attachment_access = Webform::load('test_attachment_access');
+    $sid = $this->postSubmission($webform_attachment_access);
+    $webform_submission = WebformSubmission::load($sid);
+
+    // Check access to anonymous attachment allowed via $element access rules.
+    $this->drupalGet("/admin/structure/webform/manage/test_attachment_url/submission/$sid");
+    $this->assertRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/anonymous/anonymous.txt">anonymous.txt</a>');
+    $this->assertNoRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/authenticated/authenticated.txt">authenticated.txt</a>');
+    $this->assertNoRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/private/private.txt">private.txt</a>');
+
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/anonymous/anonymous.txt");
+    $this->assertResponse(200, 'Access allowed to anonymous.txt');
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/authenticated/authenticated.txt");
+    $this->assertResponse(403, 'Access denied to authenticated.txt');
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/private/private.txt");
+    $this->assertResponse(403, 'Access denied to private.txt');
+
+    // Switch to authenticated user and set user as the submission's owner.
+    $account = $this->createUser();
+    $webform_submission->setOwnerId($account->id())->save();
+    $this->drupalLogin($account);
+
+    // Check access to authenticated attachment allowed via $element access rules.
+    $this->drupalGet("/admin/structure/webform/manage/test_attachment_url/submission/$sid");
+    $this->assertNoRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/anonymous/anonymous.txt">anonymous.txt</a>');
+    $this->assertRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/authenticated/authenticated.txt">authenticated.txt</a>');
+    $this->assertNoRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/private/private.txt">private.txt</a>');
+
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/anonymous/anonymous.txt");
+    $this->assertResponse(403, 'Access denied to anonymous.txt');
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/authenticated/authenticated.txt");
+    $this->assertResponse(200, 'Access allow to authenticated.txt');
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/private/private.txt");
+    $this->assertResponse(403, 'Access denied to private.txt');
+
+    // Switch to admin user.
+    $this->drupalLogin($this->rootUser);
+
+    // Check access to all attachment allowed for admin.
+    $this->drupalGet("/admin/structure/webform/manage/test_attachment_url/submission/$sid");
+    $this->assertNoRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/anonymous/anonymous.txt">anonymous.txt</a>');
+    $this->assertRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/authenticated/authenticated.txt">authenticated.txt</a>');
+    $this->assertRaw('<a href="' . $base_url . '/webform/' . $webform_id . '/submissions/' . $sid . '/attachment/private/private.txt">private.txt</a>');
+
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/anonymous/anonymous.txt");
+    $this->assertResponse(403, 'Access denied to anonymous.txt');
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/authenticated/authenticated.txt");
+    $this->assertResponse(200, 'Access allowed to authenticated.txt');
+    $this->drupalGet("/webform/$webform_id/submissions/$sid/attachment/private/private.txt");
+    $this->assertResponse(200, 'Access allowed to private.txt');
+
+    /**************************************************************************/
+    // Sanitize.
+    /**************************************************************************/
+
+    $webform_attachment_santize = Webform::load('test_attachment_sanitize');
+
+    $sid = $this->postSubmissionTest($webform_attachment_santize, ['textfield' => 'Some text!@#$%^&*)']);
+    $webform_submission = WebformSubmission::load($sid);
+    $element = $webform_attachment_santize->getElement('webform_attachment_token');
+    $this->assertEqual(WebformAttachmentToken::getFileName($element, $webform_submission), 'some-text.txt');
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_access.yml b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_access.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dfac1f6884ff451985f5cb1b8f1ddbd3dab46fe9
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_access.yml
@@ -0,0 +1,205 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_attachment_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_attachment_access
+title: 'Test: Webform Attachment Access'
+description: 'Test Webform attachment element access.'
+category: 'Test: Attachment'
+elements: |
+  anonymous:
+    '#type': webform_attachment_token
+    '#title': anonymous
+    '#access_create_roles':
+      - anonymous
+    '#access_update_roles':
+      - anonymous
+    '#access_view_roles':
+      - anonymous
+    '#filename': anonymous.txt
+    '#template': 'Anonymous users can access this attachment'
+    '#display_on': both
+  authenticated:
+    '#type': webform_attachment_token
+    '#title': authenticated
+    '#access_create_roles':
+      - authenticated
+    '#access_update_roles':
+      - authenticated
+    '#access_view_roles':
+      - authenticated
+    '#filename': authenticated.txt
+    '#template': 'Authenticated users can access this attachment'
+    '#display_on': both
+  private:
+    '#type': webform_attachment_token
+    '#title': private
+    '#filename': private.txt
+    '#template': 'Admin users can access this attachment'
+    '#private': true
+    '#display_on': both
+
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_email.yml b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_email.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a97f9ed2edf38b18aaa6226746b0fed5bb760bd0
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_email.yml
@@ -0,0 +1,259 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_attachment_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_attachment_email
+title: 'Test: Webform Attachment Email'
+description: 'Test Webform attachment element email.'
+category: 'Test: Attachment'
+elements: |
+  email:
+    '#type': email
+    '#title': email
+    '#required': true
+    '#default_value': johnsmith@example.com
+    '#xml_name': Emailaddress
+  first_name:
+    '#type': textfield
+    '#title': first_name
+    '#required': true
+    '#default_value': John
+    '#xml_name': Firstname
+  last_name:
+    '#type': textfield
+    '#title': last_name
+    '#required': true
+    '#default_value': Smith
+    '#xml_name': Lastname
+  attachment_token:
+    '#type': webform_attachment_token
+    '#title': attachment_token
+    '#filename': 'attachment_token-[current-date:html_date].xml'
+    '#trim': trim
+    '#template': |
+      <?xml version="1.0" encoding="UTF-8"?>
+      <asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
+         <asx:values>
+            <VERSION>1.0</VERSION>
+            <SENDER>[webform_submission:values:email:xmlencode:clear]</SENDER>
+            <WEBFORM_ID>[webform:id:xmlencode]</WEBFORM_ID>
+            <SOURCE>
+               <o2PARAVALU>
+                  <NAME>Lastname</NAME>
+                  <VALUE>[webform_submission:values:last_name:xmlencode:clear]</VALUE>
+               </o2PARAVALU>
+               <o2PARAVALU>
+                  <NAME>Firstname</NAME>
+                  <VALUE>[webform_submission:values:first_name:xmlencode:clear]</VALUE>
+               </o2PARAVALU>
+               <o2PARAVALU>
+                  <NAME>Emailaddress</NAME>
+                  <VALUE>[webform_submission:values:email:xmlencode:clear]</VALUE>
+               </o2PARAVALU>
+            </SOURCE>
+         </asx:values>
+      </asx:abap>
+      
+  attachment_twig:
+    '#type': webform_attachment_twig
+    '#title': attachment_twig
+    '#filename': 'attachment_twig-[current-date:html_date].xml'
+    '#template': "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<asx:abap xmlns:asx=\"http://www.sap.com/abapxml\" version=\"1.0\">\r\n   <asx:values>\r\n      <VERSION>1.0</VERSION>\r\n      <SENDER>{{ webform_id }}</SENDER>\r\n      <WEBFORM_ID>{{ data.email }}</WEBFORM_ID>\r\n      <SOURCE>\r\n        {% for key, value in data %}\r\n         <o2PARAVALU>\r\n            <NAME>{{ (elements_flattened[key]['#xml_name']) ? elements_flattened[key]['#xml_name'] : key }}</NAME>\r\n            <VALUE>{{ value }}</VALUE>\r\n         </o2PARAVALU>\r\n        {% endfor %}      \r\n      </SOURCE>\r\n   </asx:values>\r\n</asx:abap>"
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  email:
+    id: email
+    label: Email
+    handler_id: email
+    status: true
+    conditions: {  }
+    weight: 0
+    settings:
+      states:
+        - completed
+      to_mail: _default
+      to_options: {  }
+      cc_mail: ''
+      cc_options: {  }
+      bcc_mail: ''
+      bcc_options: {  }
+      from_mail: _default
+      from_options: {  }
+      from_name: _default
+      subject: _default
+      body: _default
+      excluded_elements: {  }
+      ignore_access: false
+      exclude_empty: true
+      exclude_empty_checkbox: false
+      html: true
+      attachments: true
+      twig: false
+      debug: true
+      reply_to: ''
+      return_path: ''
+      sender_mail: ''
+      sender_name: ''
+      theme_name: ''
diff --git a/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_sanitize.yml b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_sanitize.yml
new file mode 100644
index 0000000000000000000000000000000000000000..51ab5f7908c7ff3c8d6d3e9543559a688daa3704
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_sanitize.yml
@@ -0,0 +1,183 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_attachment_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_attachment_sanitize
+title: 'Test: Webform Attachment Filename Sanitize'
+description: 'Test Webform attachment filename sanitize element.'
+category: 'Test: Attachment'
+elements: |
+  textfield:
+    '#type': textfield
+    '#title': textfield
+    '#default_value': 'Some text!@#$%^&*'
+  webform_attachment_token:
+    '#type': webform_attachment_token
+    '#title': webform_attachment_token
+    '#filename': '[webform_submission:values:textfield:raw].txt'
+    '#sanitize': true
+    '#template': |
+      textfield: [webform_submission:values:textfield]
+
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_token.yml b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_token.yml
new file mode 100644
index 0000000000000000000000000000000000000000..07e149e49989d5de4b12c5c4f8a7a8b3fcc7a7b0
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_token.yml
@@ -0,0 +1,191 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_attachment_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_attachment_token
+title: 'Test: Webform Attachment Token'
+description: 'Test Webform attachment token element.'
+category: 'Test: Attachment'
+elements: |
+  textfield:
+    '#type': textfield
+    '#title': textfield
+    '#default_value': '{sometext}'
+  webform_attachment_token:
+    '#type': webform_attachment_token
+    '#title': webform_attachment_token
+    '#filename': test_token.txt
+    '#template': |
+      textfield: [webform_submission:values:textfield]
+  webform_attachment_token_download:
+    '#type': webform_attachment_token
+    '#title': webform_attachment_token_download
+    '#filename': test_token.txt
+    '#link_title': 'Download'
+    '#template': |
+      textfield: [webform_submission:values:textfield]
+    '#download': true
+    '#display_on': both
+
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_twig.yml b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_twig.yml
new file mode 100644
index 0000000000000000000000000000000000000000..12fe6da3b2a5b9a4fa83f3f2a6b15c029b6949dd
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_twig.yml
@@ -0,0 +1,192 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_attachment_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_attachment_twig
+title: 'Test: Webform Attachment Twig'
+description: 'Test Webform attachment Twig element.'
+category: 'Test: Attachment'
+elements: |
+  textfield:
+    '#type': textfield
+    '#title': textfield
+    '#default_value': '{sometext}'
+  webform_attachment_twig:
+    '#type': webform_attachment_twig
+    '#title': webform_attachment_twig
+    '#filename': test_twig.xml
+    '#template': |
+      <?xml version="1.0"?>
+      <textfield>{{ webform_token('[webform_submission:values:textfield:xmlencode]', webform_submission) }}</textfield>
+  webform_attachment_twig_download:
+    '#type': webform_attachment_twig
+    '#title': webform_attachment_twig_download
+    '#filename': test_twig.xml
+    '#link_title': 'Download'
+    '#template': |
+      <?xml version="1.0"?>
+      <textfield>{{ webform_token('[webform_submission:values:textfield:xmlencode]', webform_submission) }}</textfield>
+    '#download': true
+    '#display_on': both
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_url.yml b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_url.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9e8354bca9180ccffbc4b92c3563ee22ae4db2e0
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/config/install/webform.webform.test_attachment_url.yml
@@ -0,0 +1,193 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_attachment_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_attachment_url
+title: 'Test: Webform Attachment URL'
+description: 'Test Webform attachment URL element.'
+category: 'Test: Attachment'
+elements: |
+  textfield:
+    '#type': textfield
+    '#title': textfield
+    '#default_value': '{sometext}'
+  webform_attachment_url:
+    '#type': webform_attachment_url
+    '#title': webform_attachment_url
+    '#filename': MAINTAINERS.txt
+    '#url': ''
+  webform_attachment_path:
+    '#type': webform_attachment_url
+    '#title': webform_attachment_path
+    '#filename': durpalicon.png
+    '#url': ''
+  webform_attachment_url_download:
+    '#type': webform_attachment_url
+    '#title': webform_attachment_url_download
+    '#filename': MAINTAINERS.txt
+    '#link_title': 'Download'
+    '#url': ''
+    '#download': true
+    '#display_on': both
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/webform_attachment_test.info.yml b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/webform_attachment_test.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..be981e55e0bd205105ad4bd9206e5ac30445d925
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/webform_attachment_test.info.yml
@@ -0,0 +1,13 @@
+name: 'Webform Attachment Test'
+type: module
+description: 'Support module for webform attachment element that provides test webforms.'
+package: Testing
+# core: 8.x
+dependencies:
+  - 'webform_attachment:webform_attachment'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/webform_attachment_test.module b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/webform_attachment_test.module
new file mode 100644
index 0000000000000000000000000000000000000000..7cfc86f8d71312b33cd5c689db8a92cff6ed431b
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/tests/modules/webform_attachment_test/webform_attachment_test.module
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Support module for webform attachment element that provides test webforms.
+ */
+
+/**
+ * Implements hook_webform_load().
+ */
+function webform_attachment_test_webform_load(array $entities) {
+  if (!isset($entities['test_attachment_url'])) {
+    return;
+  }
+
+  global $base_url;
+
+  /** @var \Drupal\webform\WebformInterface $webform */
+  $webform = $entities['test_attachment_url'];
+  $elements = $webform->getElementsDecodedAndFlattened();
+
+  // Set absolute URL.
+  $element_keys = ['webform_attachment_url', 'webform_attachment_url_download'];
+  foreach ($element_keys as $element_key) {
+    if (isset($elements[$element_key])) {
+      $element = $elements[$element_key];
+      if (empty($element['#url'])) {
+        $element['#url'] = $base_url . '/core/MAINTAINERS.txt';
+        $webform->setElementProperties($element_key, $element);
+      }
+    }
+  }
+
+  // Set root relative URL.
+  if (isset($elements['webform_attachment_path'])) {
+    $element = $elements['webform_attachment_path'];
+    $element['#url'] = base_path() . 'core/misc/druplicon.png';
+    $webform->setElementProperties('webform_attachment_path', $element);
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_attachment/webform_attachment.info.yml b/web/modules/webform/modules/webform_attachment/webform_attachment.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7ffe0bb95236ea9687748813c68b7a4b05a5d8e4
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/webform_attachment.info.yml
@@ -0,0 +1,13 @@
+name: 'Webform Attachment'
+type: module
+description: 'Provides an element generates or loads a file that can be attached to a submission or email.'
+package: 'Webform'
+# core: 8.x
+dependencies:
+  - 'webform:webform'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_attachment/webform_attachment.routing.yml b/web/modules/webform/modules/webform_attachment/webform_attachment.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..46d74b1f1d930c939992c4309a6117a603a337dc
--- /dev/null
+++ b/web/modules/webform/modules/webform_attachment/webform_attachment.routing.yml
@@ -0,0 +1,6 @@
+entity.webform.user.submission.attachment:
+  path: '/webform/{webform}/submissions/{webform_submission}/attachment/{element}/{filename}'
+  defaults:
+    _controller: '\Drupal\webform_attachment\Controller\WebformAttachmentController::download'
+  requirements:
+    _entity_access: 'webform_submission.view'
diff --git a/web/modules/webform/modules/webform_bootstrap/README.md b/web/modules/webform/modules/webform_bootstrap/README.md
index 4b89eaf9fe5292e0bec4c4c02e97e6d26b9f1a70..f1839817472a2f6cefc4649fd088319da9910273 100644
--- a/web/modules/webform/modules/webform_bootstrap/README.md
+++ b/web/modules/webform/modules/webform_bootstrap/README.md
@@ -28,7 +28,7 @@ alert-dismissible';
 
 **Button classes**
 
-Use for 'Button CSS classes'  and 'Confirmation back link CSS classes'.
+Use for 'Button CSS classes' and 'Confirmation back link CSS classes'.
 
 ```
 btn
diff --git a/web/modules/webform/modules/webform_bootstrap/css/webform_bootstrap.css b/web/modules/webform/modules/webform_bootstrap/css/webform_bootstrap.css
index 505d317d21138cbdb5612db1d042078b20a4c420..49320ce636e7b5de25361683f2c19b36a2f9d5a2 100644
--- a/web/modules/webform/modules/webform_bootstrap/css/webform_bootstrap.css
+++ b/web/modules/webform/modules/webform_bootstrap/css/webform_bootstrap.css
@@ -39,3 +39,19 @@
   border: none;
   margin: 10px 0.5em 10px 0;
 }
+
+/**
+ * Image select.
+ */
+html.js .form-type-webform-image-select .select-wrapper:after {
+  display: none;
+}
+
+/**
+ * Issue #3010798: Responsive confirmation modal.
+ */
+@media only screen and (max-width: 650px) {
+  .webform-confirmation-modal {
+    width: 90% !important;
+  }
+}
diff --git a/web/modules/webform/modules/webform_bootstrap/js/webform.element.help.js b/web/modules/webform/modules/webform_bootstrap/js/webform.element.help.js
new file mode 100644
index 0000000000000000000000000000000000000000..f74e401a3771d5120447634874c39b8af372e9b4
--- /dev/null
+++ b/web/modules/webform/modules/webform_bootstrap/js/webform.element.help.js
@@ -0,0 +1,45 @@
+/**
+ * @file
+ * JavaScript behaviors for Bootstrap element help text (tooltip).
+ *
+ * @see js/webform.element.help.js
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  // @see http://bootstrapdocs.com/v3.0.3/docs/javascript/#tooltips-usage
+  Drupal.webformBootstrap = Drupal.webformBootstrap || {};
+  Drupal.webformBootstrap.elementHelpIcon = Drupal.webformBootstrap.elementHelpIcon || {};
+  Drupal.webformBootstrap.elementHelpIcon.options = Drupal.webformBootstrap.elementHelpIcon.options || {
+    trigger: 'hover focus click',
+    placement: 'auto right',
+    delay: 200
+  };
+
+  /**
+   * Bootstrap element help icon.
+   *
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.webformBootstrapElementHelpIcon = {
+    attach: function (context) {
+      $(context).find('.webform-element-help').once('webform-element-help').each(function () {
+        var $link = $(this);
+
+        var options = $.extend({
+          title: $link.attr('data-webform-help'),
+          html: true
+        }, Drupal.webformBootstrap.elementHelpIcon.options);
+
+        $link.tooltip(options)
+          .on('click', function (event) {
+            // Prevent click from toggling <label>s wrapped around help.
+            event.preventDefault();
+          });
+      });
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/web/modules/webform/modules/webform_bootstrap/js/webform_bootstrap.states.js b/web/modules/webform/modules/webform_bootstrap/js/webform_bootstrap.states.js
new file mode 100644
index 0000000000000000000000000000000000000000..f916f031df4664b8cabd2f1721e9cea141d94ae4
--- /dev/null
+++ b/web/modules/webform/modules/webform_bootstrap/js/webform_bootstrap.states.js
@@ -0,0 +1,28 @@
+/**
+ * @file
+ * JavaScript behaviors for custom webform #states.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  $(document).on('state:required', function (e) {
+    if (e.trigger && $(e.target).isWebform()) {
+      var $target = $(e.target);
+
+      // @see Issue #2856315: Conditional Logic - Requiring Radios in a Fieldset.
+      // Fix #required for fieldsets.
+      if ($target.is('.js-form-wrapper.panel')) {
+        if (e.value) {
+          $target.find('.panel-heading .panel-title').addClass('js-form-required form-required');
+        }
+        else {
+          $target.find('.panel-heading .panel-title').removeClass('js-form-required form-required');
+        }
+      }
+
+    }
+  });
+
+})(jQuery, Drupal);
diff --git a/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/style-guide/images.html b/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/style-guide/images.html
index 2c9629e2ffaa381424653d77e70642a994cb57c6..19643d757c2891656d2cde7fc89a36fd66345f5f 100644
--- a/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/style-guide/images.html
+++ b/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/style-guide/images.html
@@ -11,9 +11,9 @@ <h3>Responsive Images</h3>
 <h3>Image style</h3>
 
 <p>
-  <img src="http://placehold.it/200x200" alt="..." class="img-rounded">
-  <img src="http://placehold.it/200x200" alt="..." class="img-circle">
-  <img src="http://placehold.it/200x200" alt="..." class="img-thumbnail">
+  <img src="http://placehold.it/200x200" alt="…" class="img-rounded">
+  <img src="http://placehold.it/200x200" alt="…" class="img-circle">
+  <img src="http://placehold.it/200x200" alt="…" class="img-thumbnail">
 </p>
 
 <h3>Thumbnails with Captions and Links</h3>
diff --git a/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/style-guide/inputs.html b/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/style-guide/inputs.html
index c31c5926382c5453c44915f41381bf913683159e..de9a49948b90f74179a145a69b72fb7e661143ac 100644
--- a/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/style-guide/inputs.html
+++ b/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/style-guide/inputs.html
@@ -107,12 +107,12 @@ <h2><a href="http://getbootstrap.com/css/#inputs">Inputs</a></h2>
     <div class="col-sm-10">
       <div class="form-group">
         <label class="control-label" for="focusedInput">Focused input</label>
-        <input class="form-control" id="focusedInput" type="text" value="This is focused...">
+        <input class="form-control" id="focusedInput" type="text" value="This is focused…">
       </div>
 
       <div class="form-group">
         <label class="control-label" for="disabledInput">Disabled input</label>
-        <input class="form-control" id="disabledInput" type="text" placeholder="Disabled input here..." disabled="">
+        <input class="form-control" id="disabledInput" type="text" placeholder="Disabled input here…" disabled="">
       </div>
 
       <div class="form-group has-warning">
diff --git a/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/style-guide/typography.html b/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/style-guide/typography.html
index 59688123d067013873a86e82c9c10b749f0fa236..fd16237ab62f340e8561134fbe97e8a2ade5f387 100644
--- a/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/style-guide/typography.html
+++ b/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/style-guide/typography.html
@@ -33,9 +33,9 @@ <h3>Block elements</h3>
   <footer>footer <cite title="Citation Title">cite</cite></footer>
 </blockquote>
 
-<pre><b>pre(formatted)</b>&lt;p&gt;Sample text here...&lt;/p&gt;</pre>
+<pre><b>pre(formatted)</b>&lt;p&gt;Sample text here…&lt;/p&gt;</pre>
 
-<pre class=".pre-scrollable"><b>pre.pre-scrollable</b>&lt;p&gt;Sample text here...&lt;/p&gt;</pre>
+<pre class=".pre-scrollable"><b>pre.pre-scrollable</b>&lt;p&gt;Sample text here…&lt;/p&gt;</pre>
 
 <h3>Alignment classes</h3>
 
diff --git a/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/webform_bootstrap_test_module.info.yml b/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/webform_bootstrap_test_module.info.yml
index 1593010dfc2d78d4334fb8847160c97d6060015f..a11a933c33d753be12c63335d4f3456f4c1307db 100644
--- a/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/webform_bootstrap_test_module.info.yml
+++ b/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/webform_bootstrap_test_module.info.yml
@@ -8,8 +8,8 @@ dependencies:
   - 'webform:webform'
   - 'webform:webform_bootstrap'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/webform_bootstrap_test_module.libraries.yml b/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/webform_bootstrap_test_module.libraries.yml
index a8c17ee4a48865b986a815ace5914638ce1ae879..1822bc4fb73d586214a09634bc6d31405264e2f0 100644
--- a/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/webform_bootstrap_test_module.libraries.yml
+++ b/web/modules/webform/modules/webform_bootstrap/tests/modules/webform_bootstrap_test_module/webform_bootstrap_test_module.libraries.yml
@@ -4,4 +4,4 @@ webform_bootstrap_test_module:
     theme:
       css/webform_bootstrap_test_module.css: {}
   js:
-    js/webform_bootstrap_test_module.js:  {}
+    js/webform_bootstrap_test_module.js: {}
diff --git a/web/modules/webform/modules/webform_bootstrap/tests/themes/webform_bootstrap_test_theme/src/Plugin/Preprocess/Region.php b/web/modules/webform/modules/webform_bootstrap/tests/themes/webform_bootstrap_test_theme/src/Plugin/Preprocess/Region.php
index 6b48a6c6db14bc3608d6b13fa7b7dcee4a1571dc..cac9f5330e659f38b7608da198fefb7c2b499081 100644
--- a/web/modules/webform/modules/webform_bootstrap/tests/themes/webform_bootstrap_test_theme/src/Plugin/Preprocess/Region.php
+++ b/web/modules/webform/modules/webform_bootstrap/tests/themes/webform_bootstrap_test_theme/src/Plugin/Preprocess/Region.php
@@ -7,6 +7,7 @@
 use Drupal\bootstrap\Plugin\Preprocess\PreprocessInterface;
 
 if (class_exists('\Drupal\bootstrap\Plugin\Preprocess\PreprocessBase')) {
+
   /**
    * Pre-processes variables for the "region" theme hook.
    *
@@ -33,10 +34,14 @@ public function preprocessVariables(Variables $variables) {
         $variables->addClass($region_wells[$region]);
       }
     }
-
   }
 
 }
 else {
+
+  /**
+   * Empty "region" theme hook.
+   *
+   */
   class Region {}
 }
diff --git a/web/modules/webform/modules/webform_bootstrap/tests/themes/webform_bootstrap_test_theme/webform_bootstrap_test_theme.info.yml b/web/modules/webform/modules/webform_bootstrap/tests/themes/webform_bootstrap_test_theme/webform_bootstrap_test_theme.info.yml
index 5f8d75901aeaf3723ca8a41d40eb064ad8fbf02e..7108ae17be17d9adfd2abce6a1d20aa7a55389ac 100644
--- a/web/modules/webform/modules/webform_bootstrap/tests/themes/webform_bootstrap_test_theme/webform_bootstrap_test_theme.info.yml
+++ b/web/modules/webform/modules/webform_bootstrap/tests/themes/webform_bootstrap_test_theme/webform_bootstrap_test_theme.info.yml
@@ -22,8 +22,8 @@ regions:
 libraries:
   - 'webform_bootstrap_test_theme/global-styling'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_bootstrap/webform_bootstrap.info.yml b/web/modules/webform/modules/webform_bootstrap/webform_bootstrap.info.yml
index 259024596d4ee2065bdde3cb6056f21dc00a1513..261c7c044d2b81e03e72d4c53ed27c69e680345f 100644
--- a/web/modules/webform/modules/webform_bootstrap/webform_bootstrap.info.yml
+++ b/web/modules/webform/modules/webform_bootstrap/webform_bootstrap.info.yml
@@ -6,8 +6,8 @@ package: Webform
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_bootstrap/webform_bootstrap.libraries.yml b/web/modules/webform/modules/webform_bootstrap/webform_bootstrap.libraries.yml
index d5ec83d8533d10e603661c123e1582ec1a364f37..3d3e20c893a42ac4a2d7fab025f12fb36e6d7978 100644
--- a/web/modules/webform/modules/webform_bootstrap/webform_bootstrap.libraries.yml
+++ b/web/modules/webform/modules/webform_bootstrap/webform_bootstrap.libraries.yml
@@ -1,5 +1,9 @@
 webform_bootstrap:
   version: VERSION
+  js:
+    js/webform_bootstrap.states.js: {}
   css:
     theme:
       css/webform_bootstrap.css: {}
+  dependencies:
+    - webform/webform.states
diff --git a/web/modules/webform/modules/webform_bootstrap/webform_bootstrap.module b/web/modules/webform/modules/webform_bootstrap/webform_bootstrap.module
index 01b966cc4ab511796253df9ad3b0033fd7d11b16..2df214d2103827a30f7dbee4b81b117cfab89a2a 100644
--- a/web/modules/webform/modules/webform_bootstrap/webform_bootstrap.module
+++ b/web/modules/webform/modules/webform_bootstrap/webform_bootstrap.module
@@ -7,6 +7,8 @@
 
 use Drupal\Core\Asset\AttachedAssetsInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Utility\WebformArrayHelper;
+use Drupal\webform\Utility\WebformElementHelper;
 
 /**
  * Implements hook_page_attachments().
@@ -51,6 +53,18 @@ function webform_bootstrap_webform_element_alter(array &$element, FormStateInter
     $theme = \Drupal\bootstrap\Bootstrap::getTheme();
     $smart_description_enabled = $theme->getSetting('tooltip_enabled') && $theme->getSetting('forms_smart_descriptions');
   }
+
+  // We need to set $element['#smart_description'] to false if the element's
+  // description_display is set to 'before' or 'after' otherwise Bootstrap will
+  // display as a tooltip regardless of the setting.
+  if ($smart_description_enabled
+    && isset($element['#description'])
+    && isset($element['#description_display'])
+    && empty($element['#smart_description'])
+    && ($element['#description_display'] === 'after' || $element['#description_display'] === 'before')) {
+    $element['#smart_description'] = FALSE;
+  }
+
   if ($smart_description_enabled && isset($element['#description']) && is_array($element['#description']) && (empty($element['#smart_description']) || $element['#smart_description'] === TRUE)) {
     $element['#description'] = \Drupal::service('renderer')->renderPlain($element['#description']);
   }
@@ -64,22 +78,17 @@ function webform_bootstrap_webform_element_alter(array &$element, FormStateInter
 
   // Tweak element types.
   if (isset($element['#type'])) {
-    switch ($element['#type']) {
-      case 'webform_checkboxes_other':
-      case 'webform_buttons':
-      case 'webform_buttons_other':
-      case 'webform_entity_checkboxes':
-      case 'webform_entity_radios':
-      case 'webform_radios_other':
-      case 'webform_select_other':
-      case 'webform_term_checkboxes':
-      case 'webform_toggles':
-        // Prevent elements that extend radios and checkboxes from being wrapped
-        // in a fieldset.
-        // @see \Drupal\bootstrap\Plugin\Alter\ElementInfo::alter
-        $element['#bootstrap_panel'] = FALSE;
-        break;
-
+    $element_info = \Drupal::service('element_info')->getInfo($element['#type']);
+    if (isset($element_info['#pre_render'])) {
+      foreach ($element_info['#pre_render'] as $pre_render) {
+        if (is_array($pre_render) && in_array($pre_render[1], ['preRenderCompositeFormElement', 'preRenderWebformCompositeFormElement'])) {
+          // Prevent elements that extend radios and checkboxes from being wrapped
+          // in a fieldset.
+          // @see \Drupal\bootstrap\Plugin\Alter\ElementInfo::alter
+          $element['#bootstrap_panel'] = FALSE;
+          break;
+        }
+      }
     }
   }
 }
@@ -92,8 +101,17 @@ function webform_bootstrap_js_alter(&$javascript, AttachedAssetsInterface $asset
     return;
   }
 
-  // Make sure jQuery tooltip is never loaded.
-  unset($javascript['core/assets/vendor/jquery.ui/ui/tooltip-min.js']);
+  $path = drupal_get_path('module', 'webform_bootstrap');
+  // Make sure jQuery tooltip is never loaded and use custom
+  // webform.element.help.js.
+  unset(
+    $javascript['core/assets/vendor/jquery.ui/ui/tooltip-min.js'],
+    $javascript['core/assets/vendor/jquery.ui/ui/widgets/tooltip-min.js']
+  );
+
+  if (isset($javascript['modules/sandbox/webform/js/webform.element.help.js'])) {
+    $javascript['modules/sandbox/webform/js/webform.element.help.js']['data'] = $path . '/js/webform.element.help.js';
+  }
 }
 
 /**
@@ -138,6 +156,62 @@ function webform_bootstrap_preprocess_input(&$variables) {
   }
 }
 
+/**
+ * Prepares variables for form element templates.
+ */
+function webform_bootstrap_preprocess_form_element(&$variables) {
+  if (!_webform_bootstrap_is_active_theme()) {
+    return;
+  }
+
+  if (!WebformElementHelper::isWebformElement($variables['element'])) {
+    return;
+  }
+
+  // Do not display phone number using inline form.
+  // @see https://www.drupal.org/project/webform/issues/2910776s
+  // @see https://www.drupal.org/project/bootstrap/issues/2829634
+  if (WebformElementHelper::isType($variables['element'], 'tel')
+    && isset($variables['attributes']['class'])) {
+    WebformArrayHelper::removeValue($variables['attributes']['class'], 'form-inline');
+  }
+}
+
+/**
+ * Implements template_preprocess_fieldset().
+ */
+function webform_bootstrap_preprocess_fieldset(&$variables) {
+  if (!_webform_bootstrap_is_active_theme()) {
+    return;
+  }
+
+  // Make sure that the core/modules/system/templates/fieldset.html.twig
+  // template is being used because this the template that we are fixing.
+  // Caching the info just-in-case there are a lot of fieldsets.
+  static $is_system_fieldset;
+  if (!isset($is_system_fieldset)) {
+    /** @var \Drupal\Core\Utility\ThemeRegistry $theme_registry */
+    $theme_registry = \Drupal::service('theme.registry')->getRuntime();
+    $info = $theme_registry->get('fieldset');
+    $is_system_fieldset = ($info['path'] === 'core/modules/system/templates') ? TRUE : FALSE;
+  }
+  if (!$is_system_fieldset) {
+    return;
+  }
+
+  // Style fieldset error messages to match form element error messages.
+  // @see form-element.html.twig
+  if (!empty($variables['errors'])) {
+    $variables['errors'] = [
+      '#type' => 'container',
+      '#attributes' => [
+        'class' => ['form-item--error-message', 'alert', 'alert-danger', 'alert-sm'],
+      ],
+      'content' => ['#markup' => $variables['errors']],
+    ];
+  }
+}
+
 /**
  * Implements template_preprocess_file_managed_file().
  *
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/install/webform.webform.demo_application.yml b/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/install/webform.webform.demo_application.yml
index 1f6aa7bdc5cae6d531acb102eb632ae6dd70ed8c..f3c10e9e467ab1fa7bb47704aa161d339a547ed5 100644
--- a/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/install/webform.webform.demo_application.yml
+++ b/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/install/webform.webform.demo_application.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_demo_application_evaluation
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: demo_application
 title: 'Demo: Application'
 description: 'A demonstration of a very basic application form.'
@@ -77,6 +79,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -84,6 +87,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -99,22 +103,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: 'Application: [webform_submission:values:contact:name]'
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -125,6 +144,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -143,9 +163,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -199,4 +221,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/install/webform.webform.demo_application_evaluation.yml b/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/install/webform.webform.demo_application_evaluation.yml
index a87d94bccf26b228664127b9b4adc3f3befcfffa..08a639909edcf8a9f2ad2fb186301430a3efe7ab 100644
--- a/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/install/webform.webform.demo_application_evaluation.yml
+++ b/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/install/webform.webform.demo_application_evaluation.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_demo_application_evaluation
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: demo_application_evaluation
 title: 'Demo: Application Evaluation'
 description: 'A demonstration of a very basic application evaluation form. This form is attached to the ''Demo: Application'' using a Webform block.'
@@ -33,6 +35,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -40,6 +43,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -55,22 +59,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -81,6 +100,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: '1'
   draft_multiple: false
   draft_auto_save: false
@@ -99,9 +119,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: 1
@@ -154,4 +176,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/optional/block.block.bartik_webform_demo_application_evaluation.yml b/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/optional/block.block.bartik_webform_demo_application_evaluation.yml
index a31bf10575c81ffba66ecbd08d87551136c489dd..3b098e207bc63b6768aee06a1e33158be8ca84aa 100644
--- a/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/optional/block.block.bartik_webform_demo_application_evaluation.yml
+++ b/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/optional/block.block.bartik_webform_demo_application_evaluation.yml
@@ -23,6 +23,7 @@ settings:
   label_display: '0'
   webform_id: demo_application_evaluation
   default_data: ''
+  redirect: false
 visibility:
   webform:
     id: webform
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/optional/block.block.seven_webform_demo_application_evaluation.yml b/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/optional/block.block.seven_webform_demo_application_evaluation.yml
index b164b44932ecde2503bed0544b478050bb11b7be..4f71a6118dd00ceeae4df7699470cedcc60017cb 100644
--- a/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/optional/block.block.seven_webform_demo_application_evaluation.yml
+++ b/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/config/optional/block.block.seven_webform_demo_application_evaluation.yml
@@ -23,6 +23,7 @@ settings:
   label_display: '0'
   webform_id: demo_application_evaluation
   default_data: ''
+  redirect: false
 visibility:
   webform:
     id: webform
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/webform_demo_application_evaluation.info.yml b/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/webform_demo_application_evaluation.info.yml
index f4e28ec0593cc1772120d38f337d66934bfccf6e..9ef98a4605d9e2eae0193eab0d6e68e512befe7d 100644
--- a/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/webform_demo_application_evaluation.info.yml
+++ b/web/modules/webform/modules/webform_demo/webform_demo_application_evaluation/webform_demo_application_evaluation.info.yml
@@ -8,8 +8,8 @@ dependencies:
   - 'webform:webform'
   - 'webform:webform_node'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_event_registration/config/install/core.entity_view_display.node.webform_demo_event.default.yml b/web/modules/webform/modules/webform_demo/webform_demo_event_registration/config/install/core.entity_view_display.node.webform_demo_event.default.yml
index 5c3c18e93f777ce1cd620392c0f7826ce3e1e263..badc93505c5baa124d743ecad3050fea31276376 100644
--- a/web/modules/webform/modules/webform_demo/webform_demo_event_registration/config/install/core.entity_view_display.node.webform_demo_event.default.yml
+++ b/web/modules/webform/modules/webform_demo/webform_demo_event_registration/config/install/core.entity_view_display.node.webform_demo_event.default.yml
@@ -51,8 +51,13 @@ content:
     weight: 3
     label: hidden
     settings:
-      source_entity: true
+      label: Register
+      dialog: normal
+      attributes:
+        class:
+          - button
+        style: 'margin: 1em 0'
     third_party_settings: {  }
-    type: webform_entity_reference_entity_view
+    type: webform_entity_reference_link
     region: content
 hidden: {  }
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_event_registration/config/install/field.field.node.webform_demo_event.webform.yml b/web/modules/webform/modules/webform_demo/webform_demo_event_registration/config/install/field.field.node.webform_demo_event.webform.yml
index cab185471cea05154053a7a31e8502e633ac9b36..b31b786664764cfa78032a56844c8a27afc064d0 100644
--- a/web/modules/webform/modules/webform_demo/webform_demo_event_registration/config/install/field.field.node.webform_demo_event.webform.yml
+++ b/web/modules/webform/modules/webform_demo/webform_demo_event_registration/config/install/field.field.node.webform_demo_event.webform.yml
@@ -19,15 +19,11 @@ default_value:
     status: open
     open: ''
     close: ''
-    target_uuid: c9a4a1ff-ba66-454d-a572-9c2624b73a67
+    target_uuid: ''
 default_value_callback: ''
 settings:
   handler: 'default:webform'
   handler_settings:
     target_bundles: null
     auto_create: false
-  default_data: ''
-  status: open
-  open: ''
-  close: ''
 field_type: webform
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_event_registration/config/install/webform.webform.demo_event_registration.yml b/web/modules/webform/modules/webform_demo/webform_demo_event_registration/config/install/webform.webform.demo_event_registration.yml
index ecf7ab87b9c98a2dcb076a8541d05e7ee6aa4a13..78703d3cd2767155dffc308e6ff08e23a04f1a78 100644
--- a/web/modules/webform/modules/webform_demo/webform_demo_event_registration/config/install/webform.webform.demo_event_registration.yml
+++ b/web/modules/webform/modules/webform_demo/webform_demo_event_registration/config/install/webform.webform.demo_event_registration.yml
@@ -8,8 +8,10 @@ dependencies:
     - webform_scheduled_email
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: demo_event_registration
 title: 'Demo: Event Registration'
 description: 'A demonstration of an event registration form.'
@@ -53,6 +55,7 @@ settings:
   page: false
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -60,6 +63,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -75,22 +79,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: true
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -101,6 +120,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -119,9 +139,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -166,6 +188,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_notification:
     id: email
@@ -177,7 +203,7 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
@@ -186,14 +212,16 @@ handlers:
       from_mail: '[webform_submission:values:email:raw]'
       from_options: {  }
       from_name: '[webform_submission:values:first_name] [webform_submission:values:last_name]'
-      subject: default
-      body: default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
@@ -209,23 +237,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: '[webform_submission:node:title:value]: Reminder'
-      body: default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
@@ -242,10 +272,10 @@ handlers:
     conditions: {  }
     weight: -50
     settings:
-      debug: ''
-      entity_limit_total: '[webform_submission:node:field_webform_entity_limit_total]'
       preview_title: ''
       preview_message: ''
       confirmation_url: ''
       confirmation_title: ''
       confirmation_message: ''
+      debug: ''
+      entity_limit_total: '[webform_submission:node:field_webform_entity_limit_total]'
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_event_registration/webform_demo_event_registration.info.yml b/web/modules/webform/modules/webform_demo/webform_demo_event_registration/webform_demo_event_registration.info.yml
index f3c50cc63b7214c44d873bfd2884584cfc5b7e10..a46aa6c7d9ffd2a46942688f9421da965c8f3876 100644
--- a/web/modules/webform/modules/webform_demo/webform_demo_event_registration/webform_demo_event_registration.info.yml
+++ b/web/modules/webform/modules/webform_demo/webform_demo_event_registration/webform_demo_event_registration.info.yml
@@ -10,8 +10,8 @@ dependencies:
   - 'webform:webform_node'
   - 'webform:webform_scheduled_email'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_event_registration/webform_demo_event_registration.install b/web/modules/webform/modules/webform_demo/webform_demo_event_registration/webform_demo_event_registration.install
index bee165532ab35631d52f34a1970ca5c2539dd232..07c9b9959c12e741621b6b2348fe2ed119d1e702 100644
--- a/web/modules/webform/modules/webform_demo/webform_demo_event_registration/webform_demo_event_registration.install
+++ b/web/modules/webform/modules/webform_demo/webform_demo_event_registration/webform_demo_event_registration.install
@@ -2,7 +2,7 @@
 
 /**
  * @file
- * Install, update and uninstall functions for the webform node module.
+ * Install, update and uninstall functions for the webform demo event registration module.
  */
 
 use Drupal\Core\Datetime\DrupalDateTime;
@@ -37,8 +37,9 @@ function webform_demo_event_registration_install() {
  * Implements hook_uninstall().
  */
 function webform_demo_event_registration_uninstall() {
-  // Delete all webform:demo_application nodes.
+  // Delete all webform:demo_event_registration nodes.
   $entity_ids = \Drupal::entityQuery('node')
+    ->condition('type', 'webform_demo_event')
     ->condition('webform.target_id', 'demo_event_registration')
     ->execute();
   if ($entity_ids) {
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/core.entity_form_display.node.webform_demo_region.default.yml b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/core.entity_form_display.node.webform_demo_region.default.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9a5682986d47b2ab59545fd3d2c44ed20786efe6
--- /dev/null
+++ b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/core.entity_form_display.node.webform_demo_region.default.yml
@@ -0,0 +1,83 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.field.node.webform_demo_region.body
+    - field.field.node.webform_demo_region.webform
+    - node.type.webform_demo_region
+  module:
+    - path
+    - text
+    - webform
+id: node.webform_demo_region.default
+targetEntityType: node
+bundle: webform_demo_region
+mode: default
+content:
+  body:
+    type: text_textarea_with_summary
+    weight: 2
+    settings:
+      rows: 9
+      summary_rows: 3
+      placeholder: ''
+    third_party_settings: {  }
+    region: content
+  created:
+    type: datetime_timestamp
+    weight: 6
+    region: content
+    settings: {  }
+    third_party_settings: {  }
+  path:
+    type: path
+    weight: 9
+    region: content
+    settings: {  }
+    third_party_settings: {  }
+  promote:
+    type: boolean_checkbox
+    settings:
+      display_label: true
+    weight: 7
+    region: content
+    third_party_settings: {  }
+  status:
+    type: boolean_checkbox
+    settings:
+      display_label: true
+    weight: 10
+    region: content
+    third_party_settings: {  }
+  sticky:
+    type: boolean_checkbox
+    settings:
+      display_label: true
+    weight: 8
+    region: content
+    third_party_settings: {  }
+  title:
+    type: string_textfield
+    weight: 0
+    region: content
+    settings:
+      size: 60
+      placeholder: ''
+    third_party_settings: {  }
+  uid:
+    type: entity_reference_autocomplete
+    weight: 5
+    settings:
+      match_operator: CONTAINS
+      size: 60
+      placeholder: ''
+    region: content
+    third_party_settings: {  }
+  webform:
+    weight: 3
+    settings:
+      default_data: true
+    third_party_settings: {  }
+    type: webform_entity_reference_select
+    region: content
+hidden: {  }
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/core.entity_view_display.node.webform_demo_region.default.yml b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/core.entity_view_display.node.webform_demo_region.default.yml
new file mode 100644
index 0000000000000000000000000000000000000000..57fcb3e36f7006fb90d3a3e2858a56d7dcc2c295
--- /dev/null
+++ b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/core.entity_view_display.node.webform_demo_region.default.yml
@@ -0,0 +1,36 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.field.node.webform_demo_region.body
+    - field.field.node.webform_demo_region.webform
+    - node.type.webform_demo_region
+  module:
+    - text
+    - user
+    - webform
+id: node.webform_demo_region.default
+targetEntityType: node
+bundle: webform_demo_region
+mode: default
+content:
+  body:
+    label: hidden
+    type: text_default
+    weight: 1
+    settings: {  }
+    third_party_settings: {  }
+    region: content
+  links:
+    weight: 4
+    region: content
+    settings: {  }
+    third_party_settings: {  }
+  webform:
+    weight: 3
+    label: hidden
+    settings: {  }
+    third_party_settings: {  }
+    type: webform_entity_reference_entity_view
+    region: content
+hidden: {  }
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/core.entity_view_display.node.webform_demo_region.teaser.yml b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/core.entity_view_display.node.webform_demo_region.teaser.yml
new file mode 100644
index 0000000000000000000000000000000000000000..83f2da8f6e386d3a602c839310e393cf1f74a14d
--- /dev/null
+++ b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/core.entity_view_display.node.webform_demo_region.teaser.yml
@@ -0,0 +1,31 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - core.entity_view_mode.node.teaser
+    - field.field.node.webform_demo_region.body
+    - field.field.node.webform_demo_region.webform
+    - node.type.webform_demo_region
+  module:
+    - text
+    - user
+id: node.webform_demo_region.teaser
+targetEntityType: node
+bundle: webform_demo_region
+mode: teaser
+content:
+  body:
+    label: hidden
+    type: text_summary_or_trimmed
+    weight: 1
+    settings:
+      trim_length: 600
+    third_party_settings: {  }
+    region: content
+  links:
+    weight: 2
+    region: content
+    settings: {  }
+    third_party_settings: {  }
+hidden:
+  webform: true
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/field.field.node.webform_demo_region.body.yml b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/field.field.node.webform_demo_region.body.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1735ebfca500bf4e3f3cea8b553863f73229358f
--- /dev/null
+++ b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/field.field.node.webform_demo_region.body.yml
@@ -0,0 +1,21 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.storage.node.body
+    - node.type.webform_demo_region
+  module:
+    - text
+id: node.webform_demo_region.body
+field_name: body
+entity_type: node
+bundle: webform_demo_region
+label: Body
+description: ''
+required: false
+translatable: true
+default_value: {  }
+default_value_callback: ''
+settings:
+  display_summary: true
+field_type: text_with_summary
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/field.field.node.webform_demo_region.webform.yml b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/field.field.node.webform_demo_region.webform.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5e2d36d4a377692c268b7dd738d657e7a9218af5
--- /dev/null
+++ b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/field.field.node.webform_demo_region.webform.yml
@@ -0,0 +1,33 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.storage.node.webform
+    - node.type.webform_demo_region
+  module:
+    - webform
+id: node.webform_demo_region.webform
+field_name: webform
+entity_type: node
+bundle: webform_demo_region
+label: Webform
+description: ''
+required: true
+translatable: true
+default_value:
+  - default_data: ''
+    status: open
+    open: ''
+    close: ''
+    target_uuid: ''
+default_value_callback: ''
+settings:
+  handler: 'default:webform'
+  handler_settings:
+    target_bundles: null
+    auto_create: false
+  default_data: ''
+  status: open
+  open: ''
+  close: ''
+field_type: webform
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/node.type.webform_demo_region.yml b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/node.type.webform_demo_region.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1eefd06806493ec2a2cc875e6e8d5a94f2f023ba
--- /dev/null
+++ b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/node.type.webform_demo_region.yml
@@ -0,0 +1,20 @@
+langcode: en
+status: true
+dependencies:
+  enforced:
+    module:
+      - webform_demo_region_contact
+  module:
+    - menu_ui
+third_party_settings:
+  menu_ui:
+    available_menus:
+      - main
+    parent: 'main:'
+name: 'Demo: Region'
+type: webform_demo_region
+description: ''
+help: ''
+new_revision: true
+preview_mode: 1
+display_submitted: true
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/webform.webform.demo_region_contact.yml b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/webform.webform.demo_region_contact.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3a15849ef8bcf641bc97f24a50758518e5ed8178
--- /dev/null
+++ b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/install/webform.webform.demo_region_contact.yml
@@ -0,0 +1,297 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_demo_region_contact
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: demo_region_contact
+title: 'Demo: Region Contact'
+description: 'A demonstration of a region based contact form.'
+category: Demo
+elements: |
+  name:
+    '#title': 'Your Name'
+    '#type': textfield
+    '#required': true
+    '#default_value': '[current-user:display-name]'
+  email:
+    '#title': 'Your Email'
+    '#type': email
+    '#required': true
+    '#default_value': '[current-user:mail]'
+  subject:
+    '#title': Subject
+    '#type': textfield
+    '#required': true
+    '#test': 'Testing contact webform from [site:name]'
+  message:
+    '#title': Message
+    '#type': textarea
+    '#required': true
+    '#test': 'Please ignore this email.'
+  actions:
+    '#type': webform_actions
+    '#title': 'Submit button(s)'
+    '#submit__label': 'Send message'
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: url_message
+  confirmation_title: ''
+  confirmation_message: 'Your message has been sent.'
+  confirmation_url: '<front>'
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  employee_email:
+    id: email
+    label: 'Employee: Email'
+    handler_id: employee_email
+    status: true
+    conditions: {  }
+    weight: 0
+    settings:
+      states:
+        - completed
+      to_mail: '[webform_access:type:demo_region_employee]'
+      to_options: {  }
+      cc_mail: ''
+      cc_options: {  }
+      bcc_mail: ''
+      bcc_options: {  }
+      from_mail: _default
+      from_options: {  }
+      from_name: _default
+      subject: 'Demo: Region Contact: Employee'
+      body: 'This email is sent to employess.'
+      excluded_elements: {  }
+      ignore_access: false
+      exclude_empty: true
+      exclude_empty_checkbox: false
+      html: true
+      attachments: false
+      twig: false
+      theme_name: ''
+      debug: true
+      reply_to: ''
+      return_path: ''
+      sender_mail: ''
+      sender_name: ''
+  manager_email:
+    id: email
+    label: 'Manager: Email'
+    handler_id: manager_email
+    status: true
+    conditions: {  }
+    weight: 0
+    settings:
+      states:
+        - completed
+      to_mail: '[webform_access:type:demo_region_manager]'
+      to_options: {  }
+      cc_mail: ''
+      cc_options: {  }
+      bcc_mail: ''
+      bcc_options: {  }
+      from_mail: _default
+      from_options: {  }
+      from_name: _default
+      subject: 'Demo: Region Contact: Manager'
+      body: 'This email is sent to managers.'
+      excluded_elements: {  }
+      ignore_access: false
+      exclude_empty: true
+      exclude_empty_checkbox: false
+      html: true
+      attachments: false
+      twig: false
+      theme_name: ''
+      debug: true
+      reply_to: ''
+      return_path: ''
+      sender_mail: ''
+      sender_name: ''
+  everyone_email:
+    id: email
+    label: 'Everyone: Email'
+    handler_id: manager_email
+    status: true
+    conditions: {  }
+    weight: 0
+    settings:
+      states:
+        - completed
+      to_mail: '[webform_access:type]'
+      to_options: {  }
+      cc_mail: ''
+      cc_options: {  }
+      bcc_mail: ''
+      bcc_options: {  }
+      from_mail: _default
+      from_options: {  }
+      from_name: _default
+      subject: 'Demo: Region Contact: Everyone'
+      body: 'This email is sent to everyone.'
+      excluded_elements: {  }
+      ignore_access: false
+      exclude_empty: true
+      exclude_empty_checkbox: false
+      html: true
+      attachments: false
+      twig: false
+      theme_name: ''
+      debug: true
+      reply_to: ''
+      return_path: ''
+      sender_mail: ''
+      sender_name: ''
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/optional/block.block.bartik_webform_demo_region.yml b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/optional/block.block.bartik_webform_demo_region.yml
new file mode 100644
index 0000000000000000000000000000000000000000..727b3d5b39aa4a268cbde98d213c496ef29324cd
--- /dev/null
+++ b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/config/optional/block.block.bartik_webform_demo_region.yml
@@ -0,0 +1,22 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - webform_access
+  theme:
+    - bartik
+  enforced:
+    module:
+      - webform_demo_region_contact
+id: bartik_webform_demo_region
+theme: bartik
+region: sidebar_second
+weight: 0
+provider: null
+plugin: webform_access_group_entity
+settings:
+  id: webform_access_group_entity
+  label: 'My Regional Webforms'
+  provider: webform_access
+  label_display: visible
+visibility: {  }
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_region_contact/webform_demo_region_contact.info.yml b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/webform_demo_region_contact.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..597959f1ad1d3e657d6752c5740f65ab159ea69d
--- /dev/null
+++ b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/webform_demo_region_contact.info.yml
@@ -0,0 +1,17 @@
+name: 'Webform Demo: Region Contact System'
+type: module
+description: 'Demonstrate how to use the Webform module to build a region based contact system.'
+package: 'Webform Demo'
+# core: 8.x
+dependencies:
+  - 'drupal:datetime'
+  - 'token:token'
+  - 'webform:webform'
+  - 'webform:webform_node'
+  - 'webform:webform_access'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_demo/webform_demo_region_contact/webform_demo_region_contact.install b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/webform_demo_region_contact.install
new file mode 100644
index 0000000000000000000000000000000000000000..d508c23587639424bb82df7f7c12c8988c0ecec6
--- /dev/null
+++ b/web/modules/webform/modules/webform_demo/webform_demo_region_contact/webform_demo_region_contact.install
@@ -0,0 +1,150 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the webform demo region contact module.
+ */
+
+use Drupal\node\Entity\Node;
+use Drupal\user\Entity\Role;
+use Drupal\user\Entity\User;
+use Drupal\webform\WebformInterface;
+use Drupal\webform_access\Entity\WebformAccessGroup;
+use Drupal\webform_access\Entity\WebformAccessType;
+
+/**
+ * Implements hook_install().
+ */
+function webform_demo_region_contact_install() {
+  // User types.
+  $types = [
+    'manager' => [
+      'label' => t('Manager'),
+      'permissions' => [
+        'administer',
+      ],
+    ],
+    'employee' => [
+      'label' => t('Employee'),
+      'permissions' => [
+        'view_any',
+        'update_any',
+      ],
+    ],
+  ];
+
+  // U.S. regions.
+  $regions = [
+    'us_neast' => [
+      'label' => t('United States: Northeast'),
+    ],
+    'us_midwest' => [
+      'label' => t('United States: Midwest'),
+    ],
+    'us_south' => [
+      'label' => t('United States: South'),
+    ],
+    'us_west' => [
+      'label' => t('United States: West'),
+    ],
+  ];
+
+  // Create role.
+  $role = Role::create(['id' => 'demo_region', 'label' => 'Demo region']);
+  $role->grantPermission('access toolbar');
+  $role->grantPermission('view the administration theme');
+  $role->save();
+
+  // Create webform access types.
+  foreach ($types as $type_name => $type) {
+    $type += [
+      'id' => 'demo_region_' . $type_name,
+    ];
+    WebformAccessType::create($type)
+      ->save();
+  }
+
+  // Create webform access groups, nodes, and users by region.
+  foreach ($regions as $region_name => $region) {
+    // Create webform node.
+    $webform_node = Node::create([
+      'type' => 'webform_demo_region',
+      'title' => $region['label'] . ': ' . t('Contact'),
+      'status' => 1,
+    ]);
+    $webform_node->webform->target_id = 'demo_region_contact';
+    $webform_node->webform->status = WebformInterface::STATUS_OPEN;
+    $webform_node->webform->open = '';
+    $webform_node->webform->close = '';
+    $webform_node->save();
+
+    foreach ($types as $type_name => $type) {
+      $webform_access_group_id = 'demo_region_' . $region_name . '_' . $type_name;
+      $webform_access_type_id = 'demo_region_' . $type_name;
+
+      // Create region webform access group.
+      $values = [
+        'label' => $region['label'] . ': ' . $type['label'],
+        'id' => $webform_access_group_id,
+        'type' => $webform_access_type_id,
+        'permissions' => $type['permissions'],
+      ];
+      $webform_access_group = WebformAccessGroup::create($values);
+
+      // Add webform node to webform access group.
+      $webform_access_group->addEntityId('node', $webform_node->id(), 'webform', 'demo_region_contact');
+
+      // Create region type user.
+      $user = User::create([
+        'name' => $webform_access_group_id,
+        'password' => $webform_access_group_id,
+        'mail' => "$webform_access_group_id@test.com",
+        'status' => 1,
+      ]);
+      $user->addRole('demo_region');
+      $user->save();
+
+      // Add user to webform access group.
+      $webform_access_group->addUserId($user->id());
+
+      // Save webform access group.
+      $webform_access_group->save();
+    }
+  }
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function webform_demo_region_contact_uninstall() {
+  // Delete all 'webform:demo_region_contact' nodes.
+  $entity_ids = \Drupal::entityQuery('node')
+    ->condition('type', 'webform_demo_region')
+    ->condition('webform.target_id', 'demo_region_contact')
+    ->execute();
+  if ($entity_ids) {
+    /** @var \Drupal\node\Entity\Node[] $nodes */
+    $nodes = Node::loadMultiple($entity_ids);
+    foreach ($nodes as $node) {
+      $node->delete();
+    }
+  }
+
+  $entity_types = [
+    'user' => 'name',
+    'user_role' => 'id',
+    'webform_access_group' => 'id',
+    'webform_access_type' => 'id',
+  ];
+  foreach ($entity_types as $entity_type => $entity_key) {
+    $entity_ids = \Drupal::entityQuery($entity_type)
+      ->condition($entity_key, 'demo_region', 'STARTS_WITH')
+      ->execute();
+    if ($entity_ids) {
+      $entities = \Drupal::entityTypeManager()->getStorage($entity_type)->loadMultiple($entity_ids);
+      foreach ($entities as $entity) {
+        $entity->delete();
+      }
+    }
+  }
+}
diff --git a/web/modules/webform/modules/webform_devel/config/install/webform_devel.settings.yml b/web/modules/webform/modules/webform_devel/config/install/webform_devel.settings.yml
deleted file mode 100644
index 548d05b7ca6f1852df50b1f4ad5e4e7db2ea6b9c..0000000000000000000000000000000000000000
--- a/web/modules/webform/modules/webform_devel/config/install/webform_devel.settings.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-logger:
-  debug: true
diff --git a/web/modules/webform/modules/webform_devel/config/schema/webform_devel.schema.yml b/web/modules/webform/modules/webform_devel/config/schema/webform_devel.schema.yml
deleted file mode 100644
index f3770293139dafb682e1d484c5f4a1913cdeda65..0000000000000000000000000000000000000000
--- a/web/modules/webform/modules/webform_devel/config/schema/webform_devel.schema.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-# Schema for the configuration files of the Webform devel module.
-
-webform_devel.settings:
-  type: config_object
-  label: 'Webform devel settings'
-  mapping:
-    logger:
-      type: mapping
-      label: 'Logging and errors'
-      mapping:
-        debug:
-          type: boolean
-          label: 'Display debugging notices and errors on screen'
diff --git a/web/modules/webform/modules/webform_devel/drush.services.yml b/web/modules/webform/modules/webform_devel/drush.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c4e2c0d789a9fc31fb47be92099af8b554ceb9ab
--- /dev/null
+++ b/web/modules/webform/modules/webform_devel/drush.services.yml
@@ -0,0 +1,6 @@
+services:
+  webform_devel.commands:
+    class: \Drupal\webform_devel\Commands\WebformDevelCommands
+    arguments: ['@state', '@user.data']
+    tags:
+      - { name: drush.command }
diff --git a/web/modules/webform/modules/webform_devel/src/Commands/WebformDevelCommands.php b/web/modules/webform/modules/webform_devel/src/Commands/WebformDevelCommands.php
new file mode 100644
index 0000000000000000000000000000000000000000..642ce95a544044a4a247d7056b2f50ec0e1e56b3
--- /dev/null
+++ b/web/modules/webform/modules/webform_devel/src/Commands/WebformDevelCommands.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Drupal\webform_devel\Commands;
+
+use Drupal\Core\Serialization\Yaml;
+use Drupal\Core\State\State;
+use Drupal\user\UserData;
+use Drupal\webform\Utility\WebformYaml;
+use Drush\Commands\DrushCommands;
+use Drush\Exceptions\UserAbortException;
+use Psr\Log\LogLevel;
+
+/**
+ * Webform devel commandfile.
+ */
+class WebformDevelCommands extends DrushCommands {
+
+  /**
+   * Provides the state system.
+   *
+   * @var \Drupal\Core\State\State
+   */
+  protected $state;
+
+  /**
+   * The user data service.
+   *
+   * @var \Drupal\user\UserData
+   */
+  protected $userData;
+
+  /**
+   * The construct method.
+   *
+   * @param \Drupal\Core\State\State $state
+   *   Provides the state system.
+   * @param \Drupal\user\UserData $user_data
+   *   The user data service.
+   */
+  public function __construct(State $state, UserData $user_data) {
+    parent::__construct();
+    $this->state = $state;
+    $this->userData = $user_data;
+  }
+
+  /**
+   * Executes devel export config.
+   *
+   * @command webform:devel:config:update
+   * @aliases wfdcu
+   */
+  public function drush_webform_devel_config_update() {
+    module_load_include('inc', 'webform', 'includes/webform.install');
+
+    $files = file_scan_directory(drupal_get_path('module', 'webform'), '/^webform\.webform\..*\.yml$/');
+    $total = 0;
+    foreach ($files as $filename => $file) {
+      try {
+        $original_yaml = file_get_contents($filename);
+
+        $tidied_yaml = $original_yaml;
+        $data = Yaml::decode($tidied_yaml);
+
+        // Skip translated configu files which don't include a langcode.
+        // @see tests/modules/webform_test_translation/config/install/language
+        if (empty($data['langcode'])) {
+          continue;
+        }
+
+        $data = _webform_update_webform_setting($data);
+        $tidied_yaml = WebformYaml::encode($data) . PHP_EOL;
+
+        if ($tidied_yaml != $original_yaml) {
+          $this->output()->writeln(dt('Updating @file…', ['@file' => $file->filename]));
+          file_put_contents($file->uri, $tidied_yaml);
+          $total++;
+        }
+      }
+      catch (\Exception $exception) {
+        $message = 'Error parsing: ' . $file->filename . PHP_EOL . $exception->getMessage();
+        if (strlen($message) > 255) {
+          $message = substr($message, 0, 255) . '…';
+        }
+        $this->logger()->log($message, LogLevel::ERROR);
+        $this->output()->writeln($message);
+      }
+    }
+
+    if ($total) {
+      $this->output()->writeln(dt('@total webform.webform.* configuration file(s) updated.', ['@total' => $total]));
+    }
+    else {
+      $this->output()->writeln(dt('No webform.webform.* configuration files updated.'));
+    }
+  }
+
+  /**
+   * Executes webform devel reset.
+   *
+   * @command webform:devel:reset
+   * @aliases wfdr
+   *
+   * @see drush_webform_devel_reset()
+   */
+  public function drush_webform_devel_reset() {
+    if (!$this->io()->confirm(dt("Are you sure you want repair the Webform module's admin settings and webforms?"))) {
+      throw new UserAbortException();
+    }
+
+    $this->output()->writeln(dt('Resetting message closed via State API…'));
+    $this->state->delete('webform.element.message');
+
+    $this->output()->writeln(dt('Resetting message closed via User Data…'));
+    $this->userData->delete('webform', NULL, 'webform.element.message');
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_devel/src/Form/WebformDevelEntitySchemaForm.php b/web/modules/webform/modules/webform_devel/src/Form/WebformDevelEntitySchemaForm.php
index 2f9a420aab18c38e39eea65f65ec660a21fc3209..53b6833d3db7da0dfb6008072945dcc02fd71534 100644
--- a/web/modules/webform/modules/webform_devel/src/Form/WebformDevelEntitySchemaForm.php
+++ b/web/modules/webform/modules/webform_devel/src/Form/WebformDevelEntitySchemaForm.php
@@ -66,7 +66,7 @@ public function form(array $form, FormStateInterface $form_state) {
 
       foreach ($element as $key => $value) {
         if ($key === 'options') {
-          $value = implode('; ', array_slice($value, 0, 12)) . (count($value) > 12 ? '; ...' : '');
+          $value = implode('; ', array_slice($value, 0, 12)) . (count($value) > 12 ? '; …' : '');
         }
         $rows[$element_key][$key] = ['#markup' => $value];
       }
diff --git a/web/modules/webform/modules/webform_devel/src/Form/WebformDevelSubmissionApiForm.php b/web/modules/webform/modules/webform_devel/src/Form/WebformDevelSubmissionApiForm.php
index fc681fdf9d60c593194ac20dbb33ec23ed060395..c093541ee8de0a02ecd54fc72507f6613a946113 100644
--- a/web/modules/webform/modules/webform_devel/src/Form/WebformDevelSubmissionApiForm.php
+++ b/web/modules/webform/modules/webform_devel/src/Form/WebformDevelSubmissionApiForm.php
@@ -105,7 +105,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
     $form['submission']['message'] = [
       '#type' => 'webform_message',
-      '#message_message' => $this->t("Submitting the below values with trigger the %title webform's ::valdiateForm() and ::submitForm() callbacks.", ['%title' => $webform->label()]),
+      '#message_message' => $this->t("Submitting the below values will trigger the %title webform's ::validateFormValues() and ::submitFormValues() callbacks.", ['%title' => $webform->label()]),
       '#message_type' => 'warning',
     ];
     $form['submission']['values'] = [
@@ -185,7 +185,9 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
   public function submitForm(array &$form, FormStateInterface $form_state) {
     $values = $form_state->getValue('values');
     $webform_submission = WebformSubmissionForm::submitFormValues($values);
-    drupal_set_message($this->t('New submission %title added.', ['%title' => $webform_submission->label()]));
+    $this->messenger()->addStatus($this->t('New submission %title added.', [
+      '%title' => $webform_submission->label(),
+    ]));
   }
 
 }
diff --git a/web/modules/webform/modules/webform_devel/src/Logger/WebformDevelLog.php b/web/modules/webform/modules/webform_devel/src/Logger/WebformDevelLog.php
deleted file mode 100644
index d77f0052d50e941ca3e7f828d0eb11752276c108..0000000000000000000000000000000000000000
--- a/web/modules/webform/modules/webform_devel/src/Logger/WebformDevelLog.php
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php
-
-namespace Drupal\webform_devel\Logger;
-
-use Drupal\Core\Config\ConfigFactory;
-use Drupal\Core\Logger\LogMessageParserInterface;
-use Drupal\Core\Logger\RfcLoggerTrait;
-use Psr\Log\LoggerInterface;
-
-/**
- * Logs events in the watchdog database table.
- */
-class WebformDevelLog implements LoggerInterface {
-
-  use RfcLoggerTrait;
-
-  /**
-   * The configuration factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
-  /**
-   * The message's placeholders parser.
-   *
-   * @var \Drupal\Core\Logger\LogMessageParserInterface
-   */
-  protected $parser;
-
-  /**
-   * Constructs a SysLog object.
-   *
-   * @param \Drupal\Core\Config\ConfigFactory $config_factory
-   *   The configuration factory object.
-   * @param \Drupal\Core\Logger\LogMessageParserInterface $parser
-   *   The parser to use when extracting message variables.
-   */
-  public function __construct(ConfigFactory $config_factory, LogMessageParserInterface $parser) {
-    $this->configFactory = $config_factory;
-
-    $this->config = $config_factory->get('webform_devel.settings');
-    $this->parser = $parser;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function log($level, $message, array $context = []) {
-    // Never display log messages on the command line.
-    if (php_sapi_name() == 'cli') {
-      return;
-    }
-
-    // Never display log messages if the 'system.logging' 'error_level'
-    // is set to 'hide'.
-    if ($this->configFactory->get('system.logging')->get('error_level') === 'hide') {
-      return;
-    }
-
-    $debug = $this->configFactory->get('webform_devel.settings')->get('logger.debug') ?: 0;
-    if ($debug == 1 && in_array($context['channel'], ['theme', 'php'])) {
-      // Populate the message placeholders and then replace them in the message.
-      $message_placeholders = $this->parser->parseMessagePlaceholders($message, $context);
-      $message = empty($message_placeholders) ? $message : strtr($message, $message_placeholders);
-      $build = ['#markup' => $message];
-      // IMPORTANT: Do not inject the renderer into WebformDevelLog because
-      // it will cause...
-      // "LogicException: The database connection is not serializable." errors
-      // for all Ajax callbacks.
-      // @see \Drupal\Core\Render\Renderer
-      drupal_set_message(\Drupal::service('renderer')->renderPlain($build), ($level <= 3) ? 'error' : 'warning');
-    }
-  }
-
-}
diff --git a/web/modules/webform/modules/webform_devel/src/WebformDevelSchema.php b/web/modules/webform/modules/webform_devel/src/WebformDevelSchema.php
index 0e36ea72fbe7881467df364716237e2b692afe98..c19dca3a10c6f30f08c668267387455ba1d9a35a 100644
--- a/web/modules/webform/modules/webform_devel/src/WebformDevelSchema.php
+++ b/web/modules/webform/modules/webform_devel/src/WebformDevelSchema.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\webform_devel;
 
-use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Form\OptGroup;
 use Drupal\Core\Render\ElementInfoManagerInterface;
 use Drupal\Core\Render\Element\Email as EmailElement;
@@ -225,7 +224,7 @@ protected function getOptionsMaxlength(array $element) {
     $options = OptGroup::flattenOptions($element['#options']);
     $maxlength = 0;
     foreach ($options as $option_value => $option_text) {
-      $maxlength = max(Unicode::strlen($option_value), $maxlength);
+      $maxlength = max(mb_strlen($option_value), $maxlength);
     }
 
     // Check element w/ other value maxlength.
diff --git a/web/modules/webform/modules/webform_devel/webform_devel.drush.inc b/web/modules/webform/modules/webform_devel/webform_devel.drush.inc
index 3884e3159cf387da8d26a686d6dc83e799c1c1e5..f95f2cf8447de86205b32b87528e1d66f6c6e5ef 100644
--- a/web/modules/webform/modules/webform_devel/webform_devel.drush.inc
+++ b/web/modules/webform/modules/webform_devel/webform_devel.drush.inc
@@ -46,15 +46,17 @@ function webform_devel_drush_help($section) {
 
 /**
  * Implements drush_hook_COMMAND().
+ *
+ * @see \Drupal\webform_devel\Commands\WebformDevelCommands::drush_webform_devel_reset()
  */
 function drush_webform_devel_reset() {
   if (!drush_confirm(dt("Are you sure you want reset the Webform module's user data and saved state?"))) {
     return drush_user_abort();
   }
 
-  drush_print(dt('Resetting message closed via State API...'));
+  drush_print(dt('Resetting message closed via State API…'));
   \Drupal::state()->delete('webform.element.message');
 
-  drush_print(dt('Resetting message closed via User Data...'));
+  drush_print(dt('Resetting message closed via User Data…'));
   \Drupal::service('user.data')->delete('webform', NULL, 'webform.element.message');
 }
diff --git a/web/modules/webform/modules/webform_devel/webform_devel.info.yml b/web/modules/webform/modules/webform_devel/webform_devel.info.yml
index b55e7658ba954c07536f45e0d163a5a0437766bf..be11b487b86a666d3b0cc37cfdd5d750a3a97206 100644
--- a/web/modules/webform/modules/webform_devel/webform_devel.info.yml
+++ b/web/modules/webform/modules/webform_devel/webform_devel.info.yml
@@ -7,8 +7,8 @@ dependencies:
   - 'devel:devel'
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_devel/webform_devel.links.task.yml b/web/modules/webform/modules/webform_devel/webform_devel.links.task.yml
index 73c4db7dcdc5f2b3ac3eeb0334c78f1d3cbbacdf..947e3f931ebc4e8e56af8f587c6fd0794d0f13cf 100644
--- a/web/modules/webform/modules/webform_devel/webform_devel.links.task.yml
+++ b/web/modules/webform/modules/webform_devel/webform_devel.links.task.yml
@@ -1,9 +1,3 @@
-entity.webform.export_form:
-  title: 'Export'
-  route_name: entity.webform.export_form
-  parent_id: devel.entities:webform.devel_tab
-  weight: 100
-
 entity.webform.schema_form:
   title: 'Webform'
   route_name: entity.webform.schema_form
diff --git a/web/modules/webform/modules/webform_devel/webform_devel.module b/web/modules/webform/modules/webform_devel/webform_devel.module
index a349e9ac8b6c0839ba2dd5fe4920fa85f2e4f64a..3d870e15eba328f1c3f19bf9ec91d4ad244eb968 100644
--- a/web/modules/webform/modules/webform_devel/webform_devel.module
+++ b/web/modules/webform/modules/webform_devel/webform_devel.module
@@ -17,7 +17,6 @@ function webform_devel_entity_type_alter(array &$entity_types) {
     /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
     $entity_type = $entity_types['webform'];
     $handlers = $entity_type->getHandlerClasses();
-    $handlers['form']['export'] = 'Drupal\webform_devel\Form\WebformDevelEntityExportForm';
     $handlers['form']['schema'] = 'Drupal\webform_devel\Form\WebformDevelEntitySchemaForm';
     $entity_type->setHandlerClass('form', $handlers['form']);
   }
@@ -33,42 +32,6 @@ function webform_devel_form_config_single_export_form_alter(&$form, FormStateInt
   $form['config_name']['#ajax']['callback'] = '_webform_form_config_single_export_form_update_export';
 }
 
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function webform_devel_form_webform_admin_config_advanced_form_alter(&$form, FormStateInterface $form_state) {
-  $form['logger'] = [
-    '#type' => 'details',
-    '#title' => t('Logging and errors'),
-    '#open' => TRUE,
-    '#tree' => TRUE,
-  ];
-  $form['logger']['debug'] = [
-    '#type' => 'radios',
-    '#title' => t("Display debugging notices and errors on screen"),
-    '#default_value' => \Drupal::config('webform_devel.settings')->get('logger.debug') ? '1' : '0',
-    '#description' => t("Checking 'Yes' will display PHP and theme notices onscreen."),
-    '#options' => [
-      '1' => t('Yes'),
-      '0' => t('No'),
-    ],
-    '#options_display' => 'side_by_side',
-    '#required' => TRUE,
-  ];
-  $form['#submit'][] = '_webform_devel_form_webform_admin_config_advanced_form_submit';
-}
-
-/**
- * Submit callback webform devel module settings.
- */
-function _webform_devel_form_webform_admin_config_advanced_form_submit(&$form, FormStateInterface $form_state) {
-  $values = $form_state->getValues();
-  $values['logger']['debug'] = (boolean) $values['logger']['debug'];
-  \Drupal::configFactory()->getEditable('webform_devel.settings')
-    ->set('logger', $values['logger'])
-    ->save();
-}
-
 /**
  * Handles switching the export textarea and tidies exported MSK configuration.
  *
@@ -88,7 +51,7 @@ function _webform_form_config_single_export_form_update_export($form, FormStateI
   // Read the raw data for this config name, encode it, and display it.
   $value = Yaml::encode(\Drupal::service('config.storage')->read($name));
 
-  // Tidy all only MSK exported configuration...for now.
+  // Tidy all only MSK exported configuration…for now.
   if (strpos($name, 'webform') === 0) {
     $value = WebformYaml::tidy($value);
   }
diff --git a/web/modules/webform/modules/webform_devel/webform_devel.routing.yml b/web/modules/webform/modules/webform_devel/webform_devel.routing.yml
index a8735f3bc43b225c422c69d5404df280fe869104..a2395df807b5f6b3afb9c7f55fa88f14f9be4275 100644
--- a/web/modules/webform/modules/webform_devel/webform_devel.routing.yml
+++ b/web/modules/webform/modules/webform_devel/webform_devel.routing.yml
@@ -1,11 +1,3 @@
-entity.webform.export_form:
-  path: '/admin/structure/webform/manage/{webform}/export'
-  defaults:
-    _entity_form: 'webform.export'
-    _title_callback: '\Drupal\webform\Controller\WebformEntityController::title'
-  requirements:
-    _permission: 'access devel information'
-
 entity.webform.schema_form:
   path: '/admin/structure/webform/manage/{webform}/schema'
   defaults:
diff --git a/web/modules/webform/modules/webform_devel/webform_devel.services.yml b/web/modules/webform/modules/webform_devel/webform_devel.services.yml
index a0f323ac76c075a98d51096e1517f411037f49a3..6e8dd87966e6e3d5aa04a7906d6d6db81cd2083a 100644
--- a/web/modules/webform/modules/webform_devel/webform_devel.services.yml
+++ b/web/modules/webform/modules/webform_devel/webform_devel.services.yml
@@ -2,9 +2,3 @@ services:
   webform_devel.schema:
     class: Drupal\webform_devel\WebformDevelSchema
     arguments: ['@plugin.manager.element_info', '@plugin.manager.webform.element']
-
-  logger.webform_devel_log:
-    class: Drupal\webform_devel\Logger\WebformDevelLog
-    arguments: ['@config.factory', '@logger.log_message_parser']
-    tags:
-      - { name: logger }
diff --git a/web/modules/webform/modules/webform_editorial/webform_editorial.info.yml b/web/modules/webform/modules/webform_editorial/webform_editorial.info.yml
index c7901e0cd76421584782a03545e3a662929db138..d2cbaf3a166d3b5d4868da45134a2ae5b4c3116c 100644
--- a/web/modules/webform/modules/webform_editorial/webform_editorial.info.yml
+++ b/web/modules/webform/modules/webform_editorial/webform_editorial.info.yml
@@ -7,8 +7,8 @@ hidden: true
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_editorial/webform_editorial.links.menu.yml b/web/modules/webform/modules/webform_editorial/webform_editorial.links.menu.yml
index 72e3acdce339454abf2cbbf992a9d9c27426b1e7..7bb2a26bedaccc064182c616d549aa34d84d1f8f 100644
--- a/web/modules/webform/modules/webform_editorial/webform_editorial.links.menu.yml
+++ b/web/modules/webform/modules/webform_editorial/webform_editorial.links.menu.yml
@@ -45,4 +45,3 @@ webform_editorial.drush:
   parent: webform_editorial.index
   route_name: webform_editorial.drush
   weight: 40
-
diff --git a/web/modules/webform/modules/webform_example_composite/config/install/webform.webform.webform_example_composite.yml b/web/modules/webform/modules/webform_example_composite/config/install/webform.webform.webform_example_composite.yml
index 2035bdb4ecc63aa82b826cb583d9a080d7a9545a..2682fe323525614df2807d5f45af7e63caff412d 100644
--- a/web/modules/webform/modules/webform_example_composite/config/install/webform.webform.webform_example_composite.yml
+++ b/web/modules/webform/modules/webform_example_composite/config/install/webform.webform.webform_example_composite.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_example_composite
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: webform_example_composite
 title: 'Example: Webform Composite'
 description: 'An example of a custom Webform composite.'
@@ -29,6 +31,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -36,6 +39,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -51,22 +55,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -77,6 +96,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -95,9 +115,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -150,4 +172,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_example_composite/src/Tests/WebformExampleCompositeTest.php b/web/modules/webform/modules/webform_example_composite/src/Tests/WebformExampleCompositeTest.php
index 7ea2b835f40af1440944faa25449419b656a938e..0504d9737c6b312e229ebebad0177434f349b9c0 100644
--- a/web/modules/webform/modules/webform_example_composite/src/Tests/WebformExampleCompositeTest.php
+++ b/web/modules/webform/modules/webform_example_composite/src/Tests/WebformExampleCompositeTest.php
@@ -27,18 +27,18 @@ public function testWebformExampleComposite() {
     $webform = Webform::load('webform_example_composite');
 
     // Check form element rendering.
-    $this->drupalGet('webform/webform_example_composite');
+    $this->drupalGet('/webform/webform_example_composite');
     // NOTE:
     // This is a very lazy but easy way to check that the element is rendering
     // as expected.
     $this->assertRaw('<label for="edit-webform-example-composite-first-name">First name</label>');
-    $this->assertRaw('<input data-webform-composite-id="webform-example-composite--14--first_name" data-drupal-selector="edit-webform-example-composite-first-name" type="text" id="edit-webform-example-composite-first-name" name="webform_example_composite[first_name]" value="" size="60" maxlength="255" class="form-text" />');
+    $this->assertFieldById('edit-webform-example-composite-first-name');
     $this->assertRaw('<label for="edit-webform-example-composite-last-name">Last name</label>');
-    $this->assertRaw('<input data-webform-composite-id="webform-example-composite--14--last_name" data-drupal-selector="edit-webform-example-composite-last-name" type="text" id="edit-webform-example-composite-last-name" name="webform_example_composite[last_name]" value="" size="60" maxlength="255" class="form-text" />');
+    $this->assertFieldById('edit-webform-example-composite-last-name');
     $this->assertRaw('<label for="edit-webform-example-composite-date-of-birth">Date of birth</label>');
-    $this->assertRaw('<input type="date" data-drupal-selector="edit-webform-example-composite-date-of-birth" data-drupal-date-format="Y-m-d" id="edit-webform-example-composite-date-of-birth" name="webform_example_composite[date_of_birth]" value="" class="form-date" data-drupal-states="{&quot;enabled&quot;:{&quot;[data-webform-composite-id=\u0022webform-example-composite--14--first_name\u0022]&quot;:{&quot;filled&quot;:true},&quot;[data-webform-composite-id=\u0022webform-example-composite--14--last_name\u0022]&quot;:{&quot;filled&quot;:true}}}" />');
+    $this->assertFieldById('edit-webform-example-composite-date-of-birth');
     $this->assertRaw('<label for="edit-webform-example-composite-gender">Gender</label>');
-    $this->assertRaw('<select data-drupal-selector="edit-webform-example-composite-gender" id="edit-webform-example-composite-gender" name="webform_example_composite[gender]" class="form-select" data-drupal-states="{&quot;enabled&quot;:{&quot;[data-webform-composite-id=\u0022webform-example-composite--14--first_name\u0022]&quot;:{&quot;filled&quot;:true},&quot;[data-webform-composite-id=\u0022webform-example-composite--14--last_name\u0022]&quot;:{&quot;filled&quot;:true}}}"><option value="" selected="selected">- None -</option><option value="Male">Male</option><option value="Female">Female</option><option value="Transgender">Transgender</option></select>');
+    $this->assertFieldById('edit-webform-example-composite-gender');
 
     // Check webform element submission.
     $edit = [
@@ -61,10 +61,10 @@ public function testWebformExampleComposite() {
     ]);
     $this->assertEqual($webform_submission->getElementData('webform_example_composite_multiple'), [
       [
-      'first_name' => 'Jane',
-      'last_name' => 'Doe',
-      'gender' => 'Female',
-      'date_of_birth' => '1920-12-01',
+        'first_name' => 'Jane',
+        'last_name' => 'Doe',
+        'gender' => 'Female',
+        'date_of_birth' => '1920-12-01',
       ],
     ]);
   }
diff --git a/web/modules/webform/modules/webform_example_composite/webform_example_composite.info.yml b/web/modules/webform/modules/webform_example_composite/webform_example_composite.info.yml
index f316c405a8f4e0390839bab62007f1388af22aaf..01aac62753b4a6c73782d5bbe2f0826eecac87d6 100644
--- a/web/modules/webform/modules/webform_example_composite/webform_example_composite.info.yml
+++ b/web/modules/webform/modules/webform_example_composite/webform_example_composite.info.yml
@@ -6,8 +6,8 @@ package: 'Webform example'
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_example_element/config/install/webform.webform.webform_example_element.yml b/web/modules/webform/modules/webform_example_element/config/install/webform.webform.webform_example_element.yml
index a54ce36e2783f5226a85bc15d7c2638115819ebd..0ef4727063ad4101ffd2a1472a05c7eb241da58d 100644
--- a/web/modules/webform/modules/webform_example_element/config/install/webform.webform.webform_example_element.yml
+++ b/web/modules/webform/modules/webform_example_element/config/install/webform.webform.webform_example_element.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_example_element
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: webform_example_element
 title: 'Example: Webform Element'
 description: 'An example of a custom Webform element.'
@@ -28,6 +30,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -35,6 +38,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -50,22 +54,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -76,6 +95,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -94,9 +114,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -149,4 +171,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_example_element/src/Plugin/WebformElement/WebformExampleElement.php b/web/modules/webform/modules/webform_example_element/src/Plugin/WebformElement/WebformExampleElement.php
index 81c461bd97227fff7ff2033c7f8bb2ba5c2b7239..0690f0dddd672ff1b1c878c2190d85f044adf08d 100644
--- a/web/modules/webform/modules/webform_example_element/src/Plugin/WebformElement/WebformExampleElement.php
+++ b/web/modules/webform/modules/webform_example_element/src/Plugin/WebformElement/WebformExampleElement.php
@@ -54,7 +54,6 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
     // @see \Drupal\webform_example_element\Element\WebformExampleElement::processWebformElementExample
   }
 
-
   /**
    * {@inheritdoc}
    */
diff --git a/web/modules/webform/modules/webform_example_element/src/Tests/WebformExampleElementTest.php b/web/modules/webform/modules/webform_example_element/src/Tests/WebformExampleElementTest.php
index d8becefcbe620578927fa1f84f52a479ba816bbe..e2c23a7112a306c21312aca8902b0012c0b4249e 100644
--- a/web/modules/webform/modules/webform_example_element/src/Tests/WebformExampleElementTest.php
+++ b/web/modules/webform/modules/webform_example_element/src/Tests/WebformExampleElementTest.php
@@ -27,7 +27,7 @@ public function testWebformExampleElement() {
     $webform = Webform::load('webform_example_element');
 
     // Check form element rendering.
-    $this->drupalGet('webform/webform_example_element');
+    $this->drupalGet('/webform/webform_example_element');
     // NOTE:
     // This is a very lazy but easy way to check that the element is rendering
     // as expected.
diff --git a/web/modules/webform/modules/webform_example_element/webform_example_element.info.yml b/web/modules/webform/modules/webform_example_element/webform_example_element.info.yml
index b655916a4c77b5db4fe77e18c687a5f2f52e593f..ca079fab93f2a7e0656f863e77ca71bef5225ae3 100644
--- a/web/modules/webform/modules/webform_example_element/webform_example_element.info.yml
+++ b/web/modules/webform/modules/webform_example_element/webform_example_element.info.yml
@@ -6,8 +6,8 @@ package: 'Webform example'
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_example_handler/config/install/webform.webform.webform_example_handler.yml b/web/modules/webform/modules/webform_example_handler/config/install/webform.webform.webform_example_handler.yml
new file mode 100644
index 0000000000000000000000000000000000000000..15542ccd940104b48086a7bb9218c6a2c7efc4cd
--- /dev/null
+++ b/web/modules/webform/modules/webform_example_handler/config/install/webform.webform.webform_example_handler.yml
@@ -0,0 +1,188 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_example_handler
+  module:
+    - webform_example_handler
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: webform_example_handler
+title: 'Example: Webform Handler'
+description: 'An example of a custom Webform handler.'
+category: Example
+elements: |
+  value:
+    '#type': textfield
+    '#title': Value
+    '#required': true
+    '#description': 'Enter a value to displayed in a custom message.'
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  example:
+    id: example
+    label: Example
+    handler_id: example
+    status: true
+    conditions: {  }
+    weight: 0
+    settings:
+      message: 'You entered: <code>[webform_submission:values:value]</code>'
+      debug: false
diff --git a/web/modules/webform/modules/webform_example_handler/config/schema/webform_example_handler.schema.yml b/web/modules/webform/modules/webform_example_handler/config/schema/webform_example_handler.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4f19f662c27f3ff8dbfe58931ebef90d3e22e1d3
--- /dev/null
+++ b/web/modules/webform/modules/webform_example_handler/config/schema/webform_example_handler.schema.yml
@@ -0,0 +1,10 @@
+webform.handler.example:
+  type: mapping
+  label: Example
+  mapping:
+    message:
+      label: Message
+      type: string
+    debug:
+      type: boolean
+      label: 'Enable debugging'
diff --git a/web/modules/webform/modules/webform_example_handler/src/Plugin/WebformHandler/ExampleWebformHandler.php b/web/modules/webform/modules/webform_example_handler/src/Plugin/WebformHandler/ExampleWebformHandler.php
new file mode 100644
index 0000000000000000000000000000000000000000..3a2acc11b8866ebc842d7c816d31e4515eda3fd4
--- /dev/null
+++ b/web/modules/webform/modules/webform_example_handler/src/Plugin/WebformHandler/ExampleWebformHandler.php
@@ -0,0 +1,281 @@
+<?php
+
+namespace Drupal\webform_example_handler\Plugin\WebformHandler;
+
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\Render\Markup;
+use Drupal\webform\Plugin\WebformHandlerBase;
+use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformSubmissionConditionsValidatorInterface;
+use Drupal\webform\WebformSubmissionInterface;
+use Drupal\webform\WebformTokenManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Webform example handler.
+ *
+ * @WebformHandler(
+ *   id = "example",
+ *   label = @Translation("Example"),
+ *   category = @Translation("Example"),
+ *   description = @Translation("Example of a webform submission handler."),
+ *   cardinality = \Drupal\webform\Plugin\WebformHandlerInterface::CARDINALITY_SINGLE,
+ *   results = \Drupal\webform\Plugin\WebformHandlerInterface::RESULTS_IGNORED,
+ *   submission = \Drupal\webform\Plugin\WebformHandlerInterface::SUBMISSION_REQUIRED,
+ * )
+ */
+class ExampleWebformHandler extends WebformHandlerBase {
+
+  /**
+   * The token manager.
+   *
+   * @var \Drupal\webform\WebformTokenManagerInterface
+   */
+  protected $tokenManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerChannelFactoryInterface $logger_factory, ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, WebformSubmissionConditionsValidatorInterface $conditions_validator, WebformTokenManagerInterface $token_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $logger_factory, $config_factory, $entity_type_manager, $conditions_validator);
+    $this->tokenManager = $token_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('logger.factory'),
+      $container->get('config.factory'),
+      $container->get('entity_type.manager'),
+      $container->get('webform_submission.conditions_validator'),
+      $container->get('webform.token_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'message' => 'This is a custom message.',
+      'debug' => FALSE,
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    // Message.
+    $form['message'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Message settings'),
+    ];
+    $form['message']['message'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Message to be displayed when form is completed'),
+      '#default_value' => $this->configuration['message'],
+      '#required' => TRUE,
+    ];
+
+    // Development.
+    $form['development'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Development settings'),
+    ];
+    $form['development']['debug'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable debugging'),
+      '#description' => $this->t('If checked, every handler method invoked will be displayed onscreen to all users.'),
+      '#return_value' => TRUE,
+      '#default_value' => $this->configuration['debug'],
+    ];
+
+    return $this->setSettingsParents($form);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    parent::submitConfigurationForm($form, $form_state);
+    $this->configuration['message'] = $form_state->getValue('message');
+    $this->configuration['debug'] = (bool) $form_state->getValue('debug');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alterElements(array &$elements, WebformInterface $webform) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function overrideSettings(array &$settings, WebformSubmissionInterface $webform_submission) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alterForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) {
+    $this->debug(__FUNCTION__);
+    if ($value = $form_state->getValue('element')) {
+      $form_state->setErrorByName('element', $this->t('The element must be empty. You entered %value.', ['%value' => $value]));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function confirmForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) {
+    $message = $this->configuration['message'];
+    $message = $this->replaceTokens($message, $this->getWebformSubmission());
+    $this->messenger()->addStatus(Markup::create(Xss::filter($message)), FALSE);
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preCreate(array $values) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postCreate(WebformSubmissionInterface $webform_submission) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postLoad(WebformSubmissionInterface $webform_submission) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preDelete(WebformSubmissionInterface $webform_submission) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postDelete(WebformSubmissionInterface $webform_submission) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(WebformSubmissionInterface $webform_submission) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(WebformSubmissionInterface $webform_submission, $update = TRUE) {
+    $this->debug(__FUNCTION__, $update ? 'update' : 'insert');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preprocessConfirmation(array &$variables) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createHandler() {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateHandler() {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteHandler() {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createElement($key, array $element) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateElement($key, array $element, array $original_element) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteElement($key, array $element) {
+    $this->debug(__FUNCTION__);
+  }
+
+  /**
+   * Display the invoked plugin method to end user.
+   *
+   * @param string $method_name
+   *   The invoked method name.
+   * @param string $context1
+   *   Additional parameter passed to the invoked method name.
+   */
+  protected function debug($method_name, $context1 = NULL) {
+    if (!empty($this->configuration['debug'])) {
+      $t_args = [
+        '@id' => $this->getHandlerId(),
+        '@class_name' => get_class($this),
+        '@method_name' => $method_name,
+        '@context1' => $context1,
+      ];
+      $this->messenger()->addWarning($this->t('Invoked @id: @class_name:@method_name @context1', $t_args), TRUE);
+    }
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_example_handler/templates/webform-handler-example-summary.html.twig b/web/modules/webform/modules/webform_example_handler/templates/webform-handler-example-summary.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..aada6293d214b5f042ff3b04b803a32d764703df
--- /dev/null
+++ b/web/modules/webform/modules/webform_example_handler/templates/webform-handler-example-summary.html.twig
@@ -0,0 +1,20 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a summary of an example webform handler.
+ *
+ * Available variables:
+ * - settings: The current configuration for this email handler:
+ *   - message: The message.
+ *   - debug: Debugging flag.
+ * - handler: The handler information, including:
+ *   - id: The handler plugin id.
+ *   - handler_id: The handler id.
+ *   - label: The handler label.
+ *   - description: The handler description.
+ *
+ * @ingroup themeable
+ */
+#}
+{% if settings.debug %}<b class="color-error">{{ 'Debugging is enabled'|t }}</b><br />{% endif %}
+<b>Message:</b> {{ settings.message }}
diff --git a/web/modules/webform/modules/webform_example_handler/webform_example_handler.info.yml b/web/modules/webform/modules/webform_example_handler/webform_example_handler.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..00d752185752ffb41dfb489fa2e15318a3a19fb6
--- /dev/null
+++ b/web/modules/webform/modules/webform_example_handler/webform_example_handler.info.yml
@@ -0,0 +1,13 @@
+name: 'Webform Handler Example'
+type: module
+description: 'Provides an example of a webform handler.'
+package: 'Webform example'
+# core: 8.x
+dependencies:
+  - 'webform:webform'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_example_handler/webform_example_handler.module b/web/modules/webform/modules/webform_example_handler/webform_example_handler.module
new file mode 100644
index 0000000000000000000000000000000000000000..5da7371c88bab953069a025431e59047a185dd9f
--- /dev/null
+++ b/web/modules/webform/modules/webform_example_handler/webform_example_handler.module
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Provides an example of a webform handler.
+ */
+
+/**
+ * Implements hook_theme().
+ */
+function webform_example_handler_theme() {
+  return [
+    'webform_handler_example_summary' => [
+      'variables' => ['settings' => NULL, 'handler' => []],
+    ],
+  ];
+}
diff --git a/web/modules/webform/modules/webform_example_remote_post/config/install/webform.webform.example_remote_post.yml b/web/modules/webform/modules/webform_example_remote_post/config/install/webform.webform.example_remote_post.yml
index 53727b5cef8f76f00ad6cc682b9014322a93efeb..af74c02866af2a7567cfcbc4884bfb2f0323f95b 100644
--- a/web/modules/webform/modules/webform_example_remote_post/config/install/webform.webform.example_remote_post.yml
+++ b/web/modules/webform/modules/webform_example_remote_post/config/install/webform.webform.example_remote_post.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_example_remote_post
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: example_remote_post
 title: 'Example: Remote post'
 description: 'An example of a webform submission posted to a remote server.'
@@ -45,6 +47,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -52,6 +55,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -67,22 +71,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -93,6 +112,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -102,6 +122,7 @@ settings:
   confirmation_title: ''
   confirmation_message: |
     <p>Your confirmation number is [webform_submission:values:confirmation_number].</p>
+    
   confirmation_url: ''
   confirmation_attributes: {  }
   confirmation_back: true
@@ -112,9 +133,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -167,6 +190,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   remote_post:
     id: remote_post
@@ -200,20 +227,24 @@ handlers:
         confirmation_number: confirmation_number
       custom_data: |
         custom_all: true
+        
       custom_options: ''
       debug: true
-      completed_url: /webform_example_remote_post/completed
+      completed_url: '[site:url]webform_example_remote_post/completed'
       completed_custom_data: |
         custom_completed: true
-      updated_url: /webform_example_remote_post/updated
+        
+      updated_url: '[site:url]webform_example_remote_post/updated'
       updated_custom_data: |
         custom_updated: true
-      deleted_url: /webform_example_remote_post/deleted
+        
+      deleted_url: '[site:url]webform_example_remote_post/deleted'
       deleted_custom_data: |
         custom_deleted: true
+        
       draft_url: ''
       draft_custom_data: ''
       converted_url: ''
       converted_custom_data: ''
       message: ''
-      messages: { }
+      messages: {  }
diff --git a/web/modules/webform/modules/webform_example_remote_post/webform_example_remote_post.info.yml b/web/modules/webform/modules/webform_example_remote_post/webform_example_remote_post.info.yml
index 9f7717c6a076e4e816481b1805c36fdb9de1f12c..444b354aaf62fd6a11bcb1016f871bce2d89abbe 100644
--- a/web/modules/webform/modules/webform_example_remote_post/webform_example_remote_post.info.yml
+++ b/web/modules/webform/modules/webform_example_remote_post/webform_example_remote_post.info.yml
@@ -4,10 +4,11 @@ description: 'Provides an example of a webform submission posted to a remote ser
 package: 'Webform example'
 # core: 8.x
 dependencies:
+  - 'token:token'
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_example_remote_post/webform_example_remote_post.module b/web/modules/webform/modules/webform_example_remote_post/webform_example_remote_post.module
deleted file mode 100644
index fd013c46d1948f616a8342ef2fc1c48a8c004b4b..0000000000000000000000000000000000000000
--- a/web/modules/webform/modules/webform_example_remote_post/webform_example_remote_post.module
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides an example of a webform submission posted to a remote server.
- */
-
-use Drupal\Core\Url;
-
-/**
- * Implements hook_webform_load().
- *
- * Using hook_webform_load() instead of hook_install() to make sure the
- * remote post URLs are set to the correct base URL and path.
- */
-function webform_example_remote_post_webform_load(array $entities) {
-  if (isset($entities['example_remote_post']) && PHP_SAPI !== 'cli' && !in_array(\Drupal::routeMatch()->getRouteName(), ['system.modules_list', 'system.modules_list_confirm'])) {
-    // Reset remote post URL to the current base URL and base path.
-    /** @var \Drupal\webform\WebformInterface $webform */
-    $webform = $entities['example_remote_post'];
-    /** @var \Drupal\webform\Plugin\WebformHandler\RemotePostWebformHandler $handler */
-    $handler = $webform->getHandler('remote_post');
-
-    $configuration = $handler->getConfiguration();
-    $states = ['completed', 'updated', 'deleted'];
-    foreach ($states as $state) {
-      // The 'webform_example_remote_post.remote_post' will not be available
-      // during installation so wrap Url::fromRoute() in try/catch.
-      try {
-        if ($configuration['settings'][$state . '_url'] === "/webform_example_remote_post/$state") {
-          $configuration['settings'][$state . '_url'] = Url::fromRoute('webform_example_remote_post.remote_post', ['type' => $state], ['absolute' => TRUE])->toString();
-        }
-      }
-      catch (\Exception $exception) {
-        return;
-      }
-    }
-    $handler->setConfiguration($configuration);
-  }
-}
diff --git a/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_computed_elements.yml b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_computed_elements.yml
index 3ea265c845581f0faeecd3f893f0203a80cdb923..e80c61c848b993b6b65f5515333344d3dd645e73 100644
--- a/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_computed_elements.yml
+++ b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_computed_elements.yml
@@ -6,13 +6,64 @@ dependencies:
       - webform_examples
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: example_computed_elements
 title: 'Example: Computed'
 description: 'Examples of a computed elements.'
 category: Example
 elements: |
+  calculation:
+    '#title': Calculation
+    '#type': webform_wizard_page
+    '#open': true
+    calculation_section:
+      '#type': webform_section
+      '#title': 'Basic adding calculation using a ''computed twig'' element'
+      container:
+        '#type': container
+        '#attributes':
+          class:
+            - 'container-inline clearfix'
+        a:
+          '#type': number
+          '#title': a
+          '#title_display': invisible
+          '#placeholder': a
+        markup:
+          '#type': webform_markup
+          '#markup': ' + '
+        b:
+          '#type': number
+          '#title': b
+          '#title_display': invisible
+          '#placeholder': b
+        markup_01:
+          '#type': webform_markup
+          '#markup': ' = '
+        c:
+          '#type': webform_computed_twig
+          '#title': c
+          '#title_display': invisible
+          '#ajax': true
+          '#template': '{% if data.a|length and data.b|length %}{{ data.a + data.b }}{% else %}c{% endif %}'
+    message_warning:
+      '#type': webform_message
+      '#states':
+        visible:
+          ':input[name="c"]':
+            value: c
+      '#message_type': warning
+      '#message_message': 'Please enter <strong>a</strong> and <strong>b</strong> to perform a basic adding calculation.'
+    message_status:
+      '#type': webform_message
+      '#message_message': 'Thank you for entering <strong>a</strong> and <strong>b</strong> to perform basic adding calculation.'
+      '#states':
+        visible:
+          ':input[name="c"]':
+            '!value': c
   information:
     '#title': 'Your Information'
     '#type': webform_wizard_page
@@ -36,9 +87,9 @@ elements: |
     '#open': true
     computed:
       '#type': webform_computed_twig
-      '#title': 'Computed'
+      '#title': Computed
       '#title_display': hidden
-      '#value': |
+      '#template': |
         {% set attributes = create_attribute() %}
         <h2{{ attributes.setAttribute('id', 'custom').setAttribute('style', 'color:' ~ data.color) }}>
           Hello {{ data.first_name }} {{ data.last_name }}!!!
@@ -52,6 +103,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -59,6 +111,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -74,22 +127,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -100,6 +168,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -118,9 +187,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -173,4 +244,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_computed_elements_ajax.yml b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_computed_elements_ajax.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4daba14b226e27297682bb117726bfa88175af9d
--- /dev/null
+++ b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_computed_elements_ajax.yml
@@ -0,0 +1,269 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_examples
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: example_computed_elements_ajax
+title: 'Example: Computed Ajax'
+description: 'Example of a computed element using Ajax.'
+category: Example
+elements: |
+  message_warning:
+    '#type': webform_message
+    '#states':
+      visible:
+        ':input[name="c"]':
+          value: c
+    '#message_type': warning
+    '#message_message': 'Please enter <strong>a</strong> and <strong>b</strong> to perform a basic adding calculation.'
+  message_status:
+    '#type': webform_message
+    '#message_message': 'Thank you for entering <strong>a</strong> and <strong>b</strong> to perform a basic adding calculation.'
+    '#states':
+      visible:
+        ':input[name="c"]':
+          '!value': c
+  calculation:
+    '#type': fieldset
+    '#title': 'Basic adding calculation'
+    container:
+      '#type': container
+      '#attributes':
+        class:
+          - container-inline
+          - calculation
+      a:
+        '#type': number
+        '#title': a
+        '#title_display': invisible
+        '#placeholder': a
+        '#required': true
+      markup_add:
+        '#type': webform_markup
+        '#markup': ' + '
+      b:
+        '#type': number
+        '#title': b
+        '#title_display': invisible
+        '#placeholder': b
+        '#required': true
+      markup_equals:
+        '#type': webform_markup
+        '#markup': ' = '
+      c:
+        '#type': webform_computed_twig
+        '#title': c
+        '#title_display': invisible
+        '#ajax': true
+        '#template': '{% if data.a|length and data.b|length %}{{ data.a + data.b }}{% else %}c{% endif %}'
+  user_information:
+    '#type': fieldset
+    '#title': 'User information'
+    user:
+      '#type': webform_entity_select
+      '#title': User
+      '#target_type': user
+      '#selection_handler': 'default:user'
+      '#selection_settings':
+        include_anonymous: false
+        filter:
+          type: _none
+    user_ud:
+      '#type': webform_computed_token
+      '#title': 'User: ID'
+      '#ajax': true
+      '#hide_empty': true
+      '#template': '[webform_submission:values:user:entity:uid:clear]'
+    user_display_name:
+      '#type': webform_computed_token
+      '#title': 'User: Display name'
+      '#ajax': true
+      '#hide_empty': true
+      '#template': '[webform_submission:values:user:entity:display-name:clear]'
+    user_mail:
+      '#type': webform_computed_token
+      '#title': 'User: Email'
+      '#ajax': true
+      '#hide_empty': true
+      '#template': '[webform_submission:values:user:entity:mail:clear]'
+    user_url:
+      '#type': webform_computed_token
+      '#title': 'User: URL'
+      '#ajax': true
+      '#hide_empty': true
+      '#template': '[webform_submission:values:user:entity:url:clear]'
+    user_created:
+      '#type': webform_computed_token
+      '#title': 'User: Created'
+      '#ajax': true
+      '#hide_empty': true
+      '#template': '[webform_submission:values:user:entity:created:clear]'
+css: |
+  .calculation {
+    font-size: 2em;
+  }
+  .calculation input {
+    width: 4em;
+    text-align: right;
+  }
+  
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: true
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_element_states.yml b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_element_states.yml
index cea91135adc23dcdb4b6001c685c42b11d3aa4e5..ebb2450198681bd9250aa3914a1e0d4c199375f1 100644
--- a/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_element_states.yml
+++ b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_element_states.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_examples
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: example_element_states
 title: 'Example: Elements: Condition Logic'
 description: 'Examples of elements using conditional logic.'
@@ -65,11 +67,11 @@ elements: |
         1: One
         2: Two
         3: Three
-        other: Other...
+        other: Other…
     select_other:
       '#type': textfield
       '#attributes':
-        placeholder: 'Enter other...'
+        placeholder: 'Enter other…'
       '#states':
         visible:
           ':input[name="select"]':
@@ -92,7 +94,7 @@ elements: |
     select_multiple_other:
       '#type': textfield
       '#attributes':
-        placeholder: 'Enter other...'
+        placeholder: 'Enter other…'
       '#states':
         visible:
           - ':input[name="select_multiple"]':
@@ -117,11 +119,11 @@ elements: |
         1: One
         2: Two
         3: Three
-        other: Other...
+        other: Other…
     radios_other:
       '#type': textfield
       '#attributes':
-        placeholder: 'Enter other...'
+        placeholder: 'Enter other…'
       '#states':
         visible:
           ':input[name="radios"]':
@@ -197,6 +199,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -204,6 +207,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -219,22 +223,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -245,6 +264,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -263,9 +283,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -318,4 +340,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_flexbox_layout.yml b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_flexbox_layout.yml
index 2e630fb8522b4418b664eaee840002912c911faa..ff1d19559c55c473782c99627e5003f7e585a718 100644
--- a/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_flexbox_layout.yml
+++ b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_flexbox_layout.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_examples
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: example_flexbox_layout
 title: 'Example: Flexbox layout'
 description: 'Examples of multiple column webform layout using <a href="https://css-tricks.com/snippets/css/a-guide-to-flexbox/">Flexbox</a>.'
@@ -43,7 +45,7 @@ elements: |
         - day
         - year
     gender:
-      '#type': radios
+      '#type': webform_radios_other
       '#title': Gender
       '#options': gender
       '#options_display': two_columns
@@ -90,7 +92,7 @@ elements: |
       '#options': state_province_names
     zip:
       '#type': textfield
-      '#title': Zip
+      '#title': ZIP
   address_line_4:
     country:
       '#type': select
@@ -123,6 +125,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -130,6 +133,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -145,22 +149,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -171,6 +190,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -189,9 +209,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -244,4 +266,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_input_masks.yml b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_input_masks.yml
index 43b1a9cbfc872046a04fa53959cf4b57f92b540c..33521b63cf4618c497c7361e2a0883173662bdda 100644
--- a/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_input_masks.yml
+++ b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_input_masks.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_examples
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: example_input_masks
 title: 'Example: Input Masks'
 description: 'Examples of elements with <a href="https://github.com/RobinHerbots/jquery.inputmask">input masks</a>.'
@@ -23,7 +25,7 @@ elements: |
       '#input_mask': '(999) 999-9999'
     zip:
       '#type': textfield
-      '#title': 'Zip code'
+      '#title': 'ZIP Code'
       '#input_mask': '99999[-9999]'
     ssn:
       '#type': textfield
@@ -41,10 +43,10 @@ elements: |
       '#type': textfield
       '#title': Email
       '#input_mask': '''alias'': ''email'''
-    date:
+    datetime:
       '#type': textfield
-      '#title': 'Date (mm/dd/yyyy)'
-      '#input_mask': '''alias'': ''mm/dd/yyyy'''
+      '#title': 'Date time (2007-06-09''T''17:46:21)'
+      '#input_mask': '''alias'': ''datetime'''
     currency:
       '#type': textfield
       '#title': Currency
@@ -75,6 +77,14 @@ elements: |
       '#type': textfield
       '#title': 'VIN (Vehicle identification number)'
       '#input_mask': '''alias'': ''vin'''
+    uppercase:
+      '#type': textfield
+      '#title': UPPERCASE
+      '#input_mask': '''casing'': ''upper'''
+    lowercase:
+      '#type': textfield
+      '#title': lowercase
+      '#input_mask': '''casing'': ''lower'''
 css: ''
 javascript: ''
 settings:
@@ -83,6 +93,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -90,6 +101,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -105,22 +117,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -131,6 +158,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -149,9 +177,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -204,4 +234,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_style_guide.yml b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_style_guide.yml
index 97e5023188ef66b12952e5ecc80881fc97724455..fca3cc2601107e1788850201d01b54f361a7c3fa 100644
--- a/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_style_guide.yml
+++ b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_style_guide.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_examples
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: example_style_guide
 title: 'Example: Style Guide'
 description: 'Style Guide containing examples of all elements and layouts.'
@@ -79,18 +81,9 @@ elements: |
       '#title': 'Checkboxes with descriptions'
       '#options_description_display': description
       '#options':
-        one: 'One -- This is a descripion.'
-        two: 'Two -- This is a descripion.'
-        three: 'Three -- This is a descripion.'
-    checkboxes_icheck:
-      '#type': checkboxes
-      '#title': 'Checkboxes (iCheck)'
-      '#options_display': side_by_side
-      '#options':
-        one: One
-        two: Two
-        three: Three
-      '#icheck': minimal
+        one: 'One -- This is a description.'
+        two: 'Two -- This is a description.'
+        three: 'Three -- This is a description.'
     radios:
       '#type': radios
       '#title': Radios
@@ -99,15 +92,6 @@ elements: |
         one: One
         two: Two
         three: Three
-    radios_icheck:
-      '#type': radios
-      '#title': 'Radios (iCheck)'
-      '#options_display': side_by_side
-      '#options':
-        one: One
-        two: Two
-        three: Three
-      '#icheck': minimal
   date_elements:
     '#type': details
     '#title': 'Date elements'
@@ -133,6 +117,15 @@ elements: |
     '#type': details
     '#title': 'Advanced elements'
     '#open': true
+    email:
+      '#type': email
+      '#title': Email
+    email_multiple:
+      '#type': webform_email_multiple
+      '#title': 'Email multiple'
+    email_confirm:
+      '#type': webform_email_confirm
+      '#title': Email
     tel:
       '#type': tel
       '#title': Telephone
@@ -140,6 +133,7 @@ elements: |
       '#type': tel
       '#title': 'Telephone (International)'
       '#international': true
+      '#telephone_validation_format': '0'
     url:
       '#type': url
       '#title': URL
@@ -236,16 +230,6 @@ elements: |
     webform_terms_of_service:
       '#type': webform_terms_of_service
       '#terms_content': 'These are the terms of service.'
-    webform_toggle:
-      '#type': webform_toggle
-      '#title': Toggle
-    webform_toggles:
-      '#type': webform_toggles
-      '#title': Toggles
-      '#options':
-        one: One
-        two: Two
-        three: Three
     webform_likert:
       '#type': webform_likert
       '#title': Likert
@@ -344,8 +328,8 @@ elements: |
       '#type': webform_address
       '#title': Address
       '#flexbox': false
-    webform_location:
-      '#type': webform_location
+    webform_location_geocomplete:
+      '#type': webform_location_places
       '#title': Location
       '#map': true
       '#geolocation': true
@@ -366,7 +350,7 @@ elements: |
           '#type': textfield
           '#title': 'Last name'
         gender:
-          '#type': select
+          '#type': select_other
           '#options': gender
           '#title': Gender
         martial_status:
@@ -379,7 +363,7 @@ elements: |
           '#title': 'Employment status'
         age:
           '#type': number
-          '#title': 'Age'
+          '#title': Age
   form_elements:
     '#type': details
     '#title': 'Form elements'
@@ -389,12 +373,12 @@ elements: |
       '#title': 'Form element'
       '#description': '{description}'
       '#size': 5
-      '#field_prefix': '$'
+      '#field_prefix': $
       '#field_suffix': '.00'
     form_element_required:
       '#type': textfield
       '#title': 'Form element (Required)'
-      '#required': TRUE
+      '#required': true
       '#default_value': '{value}'
       '#attributes':
         class:
@@ -408,6 +392,12 @@ elements: |
       '#type': textfield
       '#title': 'Form element (Input mask: Phone)'
       '#input_mask': '(999) 999-9999'
+      '#test': ''
+    form_element_input_hide:
+      '#type': textfield
+      '#title': 'Form element (Input hiding)'
+      '#input_hide': true
+      '#default_value': '{value}'
     title_display_before:
       '#type': textfield
       '#title': 'Title displayed before'
@@ -477,9 +467,9 @@ elements: |
       '#wrapper_attributes':
         class:
           - 'container-inline clearfix'
-  divider:
+  dividers:
     '#type': details
-    '#title': 'Divider'
+    '#title': Dividers
     '#open': true
     horizontal_rule_dotted_medium:
       '#type': webform_horizontal_rule
@@ -534,7 +524,7 @@ elements: |
       '#message_close': true
   flexbox:
     '#type': details
-    '#title': 'Flexbox'
+    '#title': Flexbox
     '#open': true
     webform_flexbox:
       '#type': webform_flexbox
@@ -551,6 +541,14 @@ elements: |
         '#type': textfield
         '#title': 'Element (Flex: 3)'
         '#flex': 3
+  contrib:
+    '#type': details
+    '#title': 'Contrib modules'
+    '#open': true
+    address:
+      '#type': address
+      '#title': Address
+      '#description': '@see <a href="https://www.drupal.org/project/address">https://www.drupal.org/project/address</a>'
 css: ''
 javascript: ''
 settings:
@@ -559,6 +557,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -566,6 +565,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -581,22 +581,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: true
   wizard_progress_percentage: true
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -607,6 +622,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -625,9 +641,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -680,4 +698,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_wizard.yml b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_wizard.yml
index 2ea60bd0e0f2e22371e9b29f54f047b9df3d83aa..f28cb08527a666035d4d3cbfca23983ebd928a97 100644
--- a/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_wizard.yml
+++ b/web/modules/webform/modules/webform_examples/config/install/webform.webform.example_wizard.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_examples
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: example_wizard
 title: 'Example: Wizard'
 description: 'Example of a multiple step ''wizard'' webform.'
@@ -26,7 +28,7 @@ elements: |
       '#title': 'Last Name'
       '#type': textfield
     gender:
-      '#type': radios
+      '#type': webform_radios_other
       '#title': Gender
       '#options': gender
   contact:
@@ -61,6 +63,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -68,6 +71,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -83,22 +87,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: true
   wizard_start_label: ''
+  wizard_preview_link: true
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -109,6 +128,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: true
@@ -127,9 +147,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -182,4 +204,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_examples/webform_examples.info.yml b/web/modules/webform/modules/webform_examples/webform_examples.info.yml
index bbef013ce639c6e1f52dd3a7cfb5f0e8289a32a1..4d18dd1ada610cb564b15e1a3ef4c50f11a57828 100644
--- a/web/modules/webform/modules/webform_examples/webform_examples.info.yml
+++ b/web/modules/webform/modules/webform_examples/webform_examples.info.yml
@@ -6,8 +6,8 @@ package: 'Webform example'
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_advanced.yml b/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_advanced.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ad3c3535b6595566b85721f9cbcab91c8889803a
--- /dev/null
+++ b/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_advanced.yml
@@ -0,0 +1,365 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_examples_accessibility
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: example_accessibility_advanced
+title: 'Example: Accessibility Advanced'
+description: 'Advanced webform accessibility example.'
+category: 'Example: Accessibility'
+elements: |
+  email_elements:
+    '#type': details
+    '#title': 'Email elements'
+    '#open': true
+    email:
+      '#type': email
+      '#title': Email
+    email_multiple:
+      '#type': webform_email_multiple
+      '#title': 'Email multiple'
+    email_confirm:
+      '#type': webform_email_confirm
+      '#title': Email
+  option_elements:
+    '#type': details
+    '#title': 'Option elements'
+    '#open': true
+    select_select2:
+      '#type': select
+      '#title': Select2
+      '#options':
+        one: One
+        two: Two
+        three: Three
+      '#select2': true
+    select_select2_multiple:
+      '#type': select
+      '#title': 'Select2 multiple'
+      '#options':
+        one: One
+        two: Two
+        three: Three
+      '#select2': true
+      '#multiple': true
+    webform_buttons:
+      '#type': webform_buttons
+      '#title': Buttons
+      '#options':
+        one: One
+        two: Two
+        three: Three
+    webform_image_select:
+      '#type': webform_image_select
+      '#title': 'Image select'
+      '#show_label': true
+      '#images':
+        kitten_1:
+          text: 'Cute Kitten 1'
+          src: 'http://placekitten.com/220/200'
+        kitten_2:
+          text: 'Cute Kitten 2'
+          src: 'http://placekitten.com/180/200'
+        kitten_3:
+          text: 'Cute Kitten 3'
+          src: 'http://placekitten.com/130/200'
+    webform_likert:
+      '#type': webform_likert
+      '#title': Likert
+      '#questions':
+        q1: 'Please answer question 1?'
+        q2: 'How about now answering question 2?'
+        q3: 'Finally, here is question 3?'
+      '#answers':
+        1: 1
+        2: 2
+        3: 3
+        4: 4
+        5: 5
+    webform_autocomplete:
+      '#type': webform_autocomplete
+      '#title': Autocomplete
+      '#autocomplete_items': country_names
+  other_elements:
+    '#type': details
+    '#title': 'Other elements'
+    '#open': true
+    webform_select_other:
+      '#type': webform_select_other
+      '#title': 'Select other'
+      '#options':
+        one: One
+        two: Two
+        three: Three
+    webform_radios_other:
+      '#type': webform_radios_other
+      '#title': 'Radios other'
+      '#options':
+        one: One
+        two: Two
+        three: Three
+    webform_checkboxes_other:
+      '#type': webform_checkboxes_other
+      '#title': 'Checkboxes other'
+      '#options':
+        one: One
+        two: Two
+        three: Three
+    webform_buttons_other:
+      '#type': webform_buttons_other
+      '#title': 'Buttons other'
+      '#options':
+        one: One
+        two: Two
+        three: Three
+  input_mask_elements:
+    '#type': details
+    '#title': 'Input mask elements'
+    '#open': true
+    phone:
+      '#type': textfield
+      '#title': Phone
+      '#input_mask': '(999) 999-9999'
+  input_hide_elements:
+    '#type': details
+    '#title': 'Input hiding elements'
+    '#open': true
+    textfield_input_hide:
+      '#type': textfield
+      '#title': 'Textfield with input hiding'
+      '#input_hide': true
+      '#default_value': '{value}'
+    email_input_hide:
+      '#type': email
+      '#title': 'Email with input hiding'
+      '#input_hide': true
+      '#default_value': example@example.com
+  widget_elements:
+    '#type': details
+    '#title': 'Widget elements'
+    '#open': true
+    tel_international:
+      '#type': tel
+      '#title': 'Telephone (International)'
+      '#international': true
+      '#telephone_validation_format': '0'
+    webform_rating:
+      '#type': webform_rating
+      '#title': Rating
+    webform_signature:
+      '#type': webform_signature
+      '#title': Signature
+    webform_terms_of_service:
+      '#type': webform_terms_of_service
+      '#terms_content': 'These are the terms of service.'
+  table_elements:
+    '#type': details
+    '#title': 'Table elements'
+    '#open': true
+    tableselect:
+      '#type': tableselect
+      '#title': Tableselect
+      '#options':
+        one: One
+        two: Two
+        three: Three
+    webform_table_sort:
+      '#type': webform_table_sort
+      '#title': 'Table sort'
+      '#options':
+        one: One
+        two: Two
+        three: Three
+    webform_tableselect_sort:
+      '#type': webform_tableselect_sort
+      '#title': 'Tableselect sort'
+      '#options':
+        one: One
+        two: Two
+        three: Three
+  composite_elements:
+    '#type': details
+    '#title': 'Composite elements'
+    '#open': true
+    address:
+      '#title': Address
+      '#type': webform_address
+  multiple_elements:
+    '#type': details
+    '#title': 'Multiple elements'
+    '#open': true
+    textfield:
+      '#title': 'Text field'
+      '#type': textfield
+      '#multiple': true
+    webform_custom_composite:
+      '#type': webform_custom_composite
+      '#title': 'Custom composite'
+      '#element':
+        first_name:
+          '#type': textfield
+          '#title': 'First name'
+        last_name:
+          '#type': textfield
+          '#title': 'Last name'
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_basic.yml b/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_basic.yml
new file mode 100644
index 0000000000000000000000000000000000000000..475ba2e0ede40d37b3e20123a9258e2351cb434d
--- /dev/null
+++ b/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_basic.yml
@@ -0,0 +1,281 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_examples_accessibility
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: example_accessibility_basic
+title: 'Example: Accessibility Basic'
+description: 'Basic webform accessibility example.'
+category: 'Example: Accessibility'
+elements: |
+  text_elements:
+    '#type': details
+    '#title': 'Text elements'
+    '#open': true
+    textfield:
+      '#title': 'Text field'
+      '#type': textfield
+    textarea:
+      '#title': Textarea
+      '#type': textarea
+  option_elements:
+    '#type': details
+    '#title': 'Option elements'
+    '#open': true
+    select:
+      '#title': 'Select one'
+      '#type': select
+      '#options':
+        option_1: 'Option 1'
+        option_2: 'Option 2'
+        option_3: 'Option 3'
+    select_multiple:
+      '#title': 'Select multiple'
+      '#type': select
+      '#multiple': true
+      '#options':
+        option_1: 'Option 1'
+        option_2: 'Option 2'
+        option_3: 'Option 3'
+    radios:
+      '#title': Radios
+      '#type': radios
+      '#options':
+        option_1: 'Option 1'
+        option_2: 'Option 2'
+        option_3: 'Option 3'
+    checkboxes:
+      '#title': Checkboxes
+      '#type': checkboxes
+      '#options':
+        option_1: 'Option 1'
+        option_2: 'Option 2'
+        option_3: 'Option 3'
+    checkbox:
+      '#type': checkbox
+      '#title': Checkbox
+      '#description': 'This is a description'
+  file_elements:
+    '#type': details
+    '#title': 'File elements'
+    '#open': true
+    managed_file:
+      '#type': managed_file
+      '#title': 'Managed single file'
+    managed_file_multiple:
+      '#type': managed_file
+      '#title': 'Managed multiple file'
+      '#multiple': true
+  date_elements:
+    '#type': details
+    '#title': 'Date elements'
+    '#open': true
+    date:
+      '#type': date
+      '#title': Date
+    datetime:
+      '#type': datetime
+      '#title': Date/time
+    datelist:
+      '#type': datelist
+      '#title': 'Date list'
+    date_datepicker:
+      '#type': date
+      '#title': 'Date picker'
+      '#datepicker': true
+      '#date_date_format': 'D, m/d/Y'
+    webform_time:
+      '#type': webform_time
+      '#title': Time
+  advanced_elements:
+    '#type': details
+    '#title': 'Advanced elements'
+    '#open': true
+    color:
+      '#type': color
+      '#title': Color
+    tel:
+      '#type': tel
+      '#title': Telephone
+    url:
+      '#type': url
+      '#title': URL
+    search:
+      '#type': search
+      '#title': Search
+    number:
+      '#type': number
+      '#title': Number
+      '#min': 0
+      '#max': 10
+      '#step': 1
+    range:
+      '#type': range
+      '#title': Range
+      '#min': 0
+      '#max': 100
+      '#step': 1
+      '#output': right
+      '#output__field_prefix': $
+      '#output__field_suffix': '.00'
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_containers.yml b/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_containers.yml
new file mode 100644
index 0000000000000000000000000000000000000000..19b687ac875761ce6f441aa2f5ac645ea7d760b6
--- /dev/null
+++ b/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_containers.yml
@@ -0,0 +1,206 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_examples_accessibility
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: example_accessibility_containers
+title: 'Example: Accessibility Containers'
+description: 'Container webform accessibility example.'
+category: 'Example: Accessibility'
+elements: |
+  form_element:
+    '#type': textfield
+    '#title': 'Form element'
+    '#placeholder': 'This is a placeholder'
+    '#description': 'This is a description.'
+    '#help': 'This is help text.'
+    '#more': 'This is more text'
+    '#required': true
+    '#field_prefix': prefix
+    '#field_suffix': suffix
+  fieldset:
+    '#type': fieldset
+    '#title': Fieldset
+    '#placeholder': 'This is a placeholder'
+    '#description': 'This is a description.'
+    '#help': 'This is help text.'
+    '#more': 'This is more text'
+    '#required': true
+    '#field_prefix': prefix
+    '#field_suffix': suffix
+  details:
+    '#type': details
+    '#title': details
+    '#placeholder': 'This is a placeholder'
+    '#description': 'This is a description.'
+    '#help': 'This is help text.'
+    '#more': 'This is more text'
+    '#required': true
+    '#open': true
+  section:
+    '#type': webform_section
+    '#title': Section
+    '#help': 'This is help text'
+    '#required': true
+    '#description': 'This is a description'
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_labels.yml b/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_labels.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a725c40541c9f209039ca202b274534a75f8819e
--- /dev/null
+++ b/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_labels.yml
@@ -0,0 +1,336 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_examples_accessibility
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: example_accessibility_labels
+title: 'Example: Accessibility Labels & Descriptions'
+description: 'Example of webform label and description accessibility.'
+category: 'Example: Accessibility'
+elements: |
+  form_elements:
+    '#type': details
+    '#title': 'Form elements'
+    '#open': true
+    form_element:
+      '#type': textfield
+      '#title': 'Form element'
+      '#placeholder': 'This is a placeholder'
+      '#description': 'This is a description.'
+      '#help': 'This is help text.'
+      '#more': 'This is more text'
+      '#required': true
+      '#field_prefix': prefix
+      '#field_suffix': suffix
+    datelist_element:
+      '#type': datelist
+      '#title': Datelist
+      '#placeholder': 'This is a placeholder'
+      '#description': 'This is a description.'
+      '#help': 'This is help text.'
+      '#more': 'This is more text'
+      '#field_prefix': prefix
+      '#field_suffix': suffix
+  element_title:
+    '#type': details
+    '#title': 'Element title'
+    '#open': true
+    title_display_before:
+      '#type': textfield
+      '#title': 'Title displayed before'
+      '#title_display': before
+    title_display_after:
+      '#type': textfield
+      '#title': 'Title displayed after'
+      '#title_display': after
+    title_display_inline:
+      '#type': textfield
+      '#title': 'Title displayed inline'
+      '#title_display': inline
+      '#description': 'This is a description.'
+    title_display_inline_composite:
+      '#type': radios
+      '#title': 'Title displayed inline composite'
+      '#title_display': inline
+      '#description': 'This is a description.'
+      '#options':
+        one: One
+        two: Two
+        three: Three
+  element_description:
+    '#type': details
+    '#title': 'Element description'
+    '#open': true
+    description_display_after:
+      '#type': textfield
+      '#title': 'Description displayed after'
+      '#description': 'This is a description.'
+    description_display_before:
+      '#type': textfield
+      '#title': 'Description displayed before'
+      '#description': 'This is a description.'
+      '#description_display': before
+    description_display_tooltip:
+      '#type': textfield
+      '#title': 'Description displayed in tooltip'
+      '#description': 'This is a description.'
+      '#description_display': tooltip
+  element_help:
+    '#type': details
+    '#title': 'Element help'
+    '#open': true
+    help:
+      '#type': textfield
+      '#title': Help
+      '#help': 'This is help.'
+  element_more:
+    '#type': details
+    '#title': 'Element more'
+    '#open': true
+    more:
+      '#type': textfield
+      '#title': More
+      '#more': 'This is more text.'
+  fieldset_elements:
+    '#type': details
+    '#title': 'Fieldset elements'
+    '#open': true
+    fieldset:
+      '#type': fieldset
+      '#title': Fieldset
+      '#description': 'This is a description.'
+      '#help': 'This is help text.'
+      '#more': 'This is more text'
+      '#required': true
+      '#field_prefix': prefix
+      '#field_suffix': suffix
+    fieldset_description:
+      '#type': fieldset
+      '#title': 'Fieldset description'
+      '#description': 'This is a description.'
+    fieldset_title_invisible:
+      '#type': fieldset
+      '#title': 'Fieldset description invisible'
+      '#description': 'This is a description.'
+      '#title_display': invisible
+    fieldset_help:
+      '#type': fieldset
+      '#title': 'Fieldset help'
+      '#help': 'This is help text.'
+    fieldset_more:
+      '#type': fieldset
+      '#title': 'Fieldset more'
+      '#more': 'This is more text'
+  details_elements:
+    '#type': details
+    '#title': 'Details elements'
+    '#open': true
+    details:
+      '#type': details
+      '#title': details
+      '#description': 'This is a description.'
+      '#help': 'This is help text.'
+      '#more': 'This is more text'
+      '#required': true
+      '#open': true
+    details_description:
+      '#type': details
+      '#title': 'details description'
+      '#description': 'This is a description.'
+      '#open': true
+    details_description_before:
+      '#type': details
+      '#title': 'details description before'
+      '#description': 'This is a description.'
+      '#description_display': before
+      '#open': true
+      details_description_before_textfield:
+        '#type': textfield
+        '#title': 'Details description before textfield'
+    details_description_invisible:
+      '#type': details
+      '#title': 'details description invisible'
+      '#description': 'This is a description.'
+      '#description_display': invisible
+      '#open': true
+    details_help:
+      '#type': details
+      '#title': 'details help'
+      '#help': 'This is help text.'
+      '#open': true
+    details_more:
+      '#type': details
+      '#title': 'details more'
+      '#more': 'This is more text'
+      '#open': true
+  section_elements:
+    '#type': details
+    '#title': 'Section elements'
+    '#open': true
+    section:
+      '#type': webform_section
+      '#title': Section
+      '#help': 'This is help text'
+      '#required': true
+      '#description': 'This is a description'
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_wizard.yml b/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_wizard.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2249f2d8efa0cc0b37ce921532cea88a8be7ddf6
--- /dev/null
+++ b/web/modules/webform/modules/webform_examples_accessibility/config/install/webform.webform.example_accessibility_wizard.yml
@@ -0,0 +1,211 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_examples_accessibility
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: example_accessibility_wizard
+title: 'Example: Accessibility Wizard'
+description: 'Wizard webform accessibility example.'
+category: 'Example: Accessibility'
+elements: |
+  '#attributes':
+    data-current-page: '[webform_submission:current-page]'
+  information:
+    '#title': 'Your Information'
+    '#type': webform_wizard_page
+    '#open': true
+    first_name:
+      '#title': 'First Name'
+      '#type': textfield
+    last_name:
+      '#title': 'Last Name'
+      '#type': textfield
+    gender:
+      '#type': webform_radios_other
+      '#title': Gender
+      '#options': gender
+  contact:
+    '#title': 'Contact Information'
+    '#type': webform_wizard_page
+    '#open': true
+    email:
+      '#title': Email
+      '#type': email
+    phone:
+      '#title': Phone
+      '#type': tel
+    contact_via_phone:
+      '#type': radios
+      '#title': 'Can we contact you via phone?'
+      '#options': yes_no
+  feedback:
+    '#title': 'Your Feedback'
+    '#type': webform_wizard_page
+    '#open': true
+    comments:
+      '#type': textarea
+  actions:
+    '#type': webform_actions
+    '#title': 'Submit button(s)'
+    '#submit__label': Apply
+css: ''
+javascript: ''
+settings:
+  ajax: true
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: true
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: true
+  wizard_start_label: ''
+  wizard_preview_link: true
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 2
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: true
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/modules/webform_examples_accessibility/css/webform_examples_accessibility.css b/web/modules/webform/modules/webform_examples_accessibility/css/webform_examples_accessibility.css
new file mode 100644
index 0000000000000000000000000000000000000000..fff4e818834a3121d56582a2ca821be32ff1ac4e
--- /dev/null
+++ b/web/modules/webform/modules/webform_examples_accessibility/css/webform_examples_accessibility.css
@@ -0,0 +1,52 @@
+/**
+ * @file
+ * Webform examples accessibility styles.
+ */
+
+/**
+ * Show fieldsets.
+ */
+[role=main] fieldset {
+  border: 2px dashed red !important;
+  padding: 4px !important;
+}
+
+/**
+ * Show visually hidden (labels).
+ */
+[role=main] .visually-hidden {
+  border: 2px dashed blue !important;
+  color: blue !important;
+  padding: 4px !important;
+  position: relative !important;
+  clip: auto !important;
+  overflow: visible !important;
+  height: auto !important;
+  width: auto !important;
+}
+
+/**
+ * Show aria-labels.
+ */
+[role=main] [aria-label]:after {
+  content: "[aria-label]: " attr(aria-label);
+  border: 2px dashed green !important;
+  padding: 4px !important;
+  position: relative !important;
+  font-size: 0.8em;
+  font-weight: normal;
+  color: green;
+}
+
+/**
+ * Show titles.
+ */
+[role=main] [title]:after {
+  content: "[title]: " attr(title);
+  border: 2px dashed green !important;
+  padding: 4px !important;
+  position: relative !important;
+  font-size: 0.8em;
+  font-weight: normal;
+  color: green;
+}
diff --git a/web/modules/webform/modules/webform_examples_accessibility/webform_examples_accessibility.info.yml b/web/modules/webform/modules/webform_examples_accessibility/webform_examples_accessibility.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c764c2d07f5dbd4c00f659411f92637dbc437c9d
--- /dev/null
+++ b/web/modules/webform/modules/webform_examples_accessibility/webform_examples_accessibility.info.yml
@@ -0,0 +1,13 @@
+name: 'Webform Examples Accessibility'
+type: module
+description: 'Provides example webforms for reviewing and testing accessibility.'
+package: 'Webform example'
+# core: 8.x
+dependencies:
+  - 'drupal:webform'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_examples_accessibility/webform_examples_accessibility.libraries.yml b/web/modules/webform/modules/webform_examples_accessibility/webform_examples_accessibility.libraries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..84b34ab01b076640268cfa1a714e6da214fd5d74
--- /dev/null
+++ b/web/modules/webform/modules/webform_examples_accessibility/webform_examples_accessibility.libraries.yml
@@ -0,0 +1,5 @@
+webform_examples_accessibility:
+  version: VERSION
+  css:
+    theme:
+      css/webform_examples_accessibility.css: {}
diff --git a/web/modules/webform/modules/webform_examples_accessibility/webform_examples_accessibility.module b/web/modules/webform/modules/webform_examples_accessibility/webform_examples_accessibility.module
new file mode 100644
index 0000000000000000000000000000000000000000..b35c237c2bffc4a8cfd89197ab5be536a4c4b6df
--- /dev/null
+++ b/web/modules/webform/modules/webform_examples_accessibility/webform_examples_accessibility.module
@@ -0,0 +1,132 @@
+<?php
+
+/**
+ * @file
+ * Provides example webforms for reviewing and testing accessibility.
+ */
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\webform\Utility\WebformArrayHelper;
+
+/**
+ * Implements hook_page_attachments().
+ */
+function webform_examples_accessibility_page_attachments(array &$attachments) {
+  // Attach accessibility library which shows all fieldsets and labels.
+  if (\Drupal::request()->query->get('accessibility') == '1') {
+    $attachments['#attached']['library'][] = 'webform_examples_accessibility/webform_examples_accessibility';
+  }
+}
+
+/**
+ * Implements hook_webform_submission_form_alter().
+ *
+ * Adds button to disable/enable HTML client-side validation without have
+ * to change any webform settings.
+ *
+ * The link is only applicable to webform ids that are prefix with examples_accessibility_*.
+ */
+function webform_examples_accessibility_webform_submission_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
+  if (strpos($form['#webform_id'], 'example_accessibility_') !== 0
+    && !preg_match('/^issue_\d+$/', $form['#webform_id'])
+    && strpos($form['#webform_id'], 'test_') !== 0) {
+    return;
+  }
+
+  $form['accessibility'] = [
+    '#suffix' => '<hr/>',
+    '#weight' => -1000,
+  ];
+
+  /****************************************************************************/
+  // Accessibility.
+  /****************************************************************************/
+
+  // Get query without ajax parameters.
+  $query = \Drupal::request()->query->all();
+  unset($query['ajax_form'], $query['_wrapper_format']);
+
+  $accessibility = (\Drupal::request()->query->get('accessibility') == '1') ? TRUE : FALSE;
+
+  $form['accessibility']['accessibility'] = [
+    '#type' => 'link',
+    '#title' => $accessibility ? t('Hide accessibility') : t('Show accessibility'),
+    '#url' => Url::fromRoute('<current>', [], ['query' => ['accessibility' => $accessibility ? 0 : 1] + $query]),
+  ];
+
+  /****************************************************************************/
+  // Required.
+  /****************************************************************************/
+
+  $form['accessibility'][] = ['#markup' => ' | '];
+
+  if (\Drupal::request()->query->get('required') == '1') {
+    $required = TRUE;
+  }
+  elseif (\Drupal::request()->query->get('required') == '0') {
+    $required = FALSE;
+  }
+  else {
+    $required = NULL;
+  }
+
+  $form['accessibility']['required'] = [
+    '#type' => 'link',
+    '#title' => $required ? t('Disabled required') : t('Enable required'),
+    '#url' => Url::fromRoute('<current>', [], ['query' => ['required' => $required ? 0 : 1] + $query]),
+  ];
+
+  if ($required !== NULL) {
+    $elements = &WebformArrayHelper::flattenAssoc($form);
+    foreach ($elements as &$element) {
+      if (is_array($element) && isset($element['#type'])) {
+        $element['#required'] = $required;
+      }
+    }
+  }
+
+  /****************************************************************************/
+  // No validate.
+  /****************************************************************************/
+
+  $form['accessibility'][] = ['#markup' => ' | '];
+
+  if (\Drupal::request()->query->get('novalidate') == '1') {
+    $form['#attributes']['novalidate'] = TRUE;
+    $novalidate = TRUE;
+  }
+  else {
+    unset($form['#attributes']['novalidate']);
+    $novalidate = FALSE;
+  }
+
+  $form['accessibility']['novalidate'] = [
+    '#type' => 'link',
+    '#title' => $novalidate ? t('Enable client-side validation') : t('Disable client-side validation'),
+    '#url' => Url::fromRoute('<current>', [], ['query' => ['novalidate' => $novalidate ? 0 : 1] + $query]),
+  ];
+
+  /****************************************************************************/
+  // Inline form error.
+  /****************************************************************************/
+
+  if (\Drupal::moduleHandler()->moduleExists('inline_form_errors')) {
+    $form['accessibility'][] = ['#markup' => ' | '];
+
+    if (\Drupal::request()->query->get('disable_inline_form_errors') == '1') {
+      $form['#disable_inline_form_errors'] = TRUE;
+      $disable_inline_form_errors = TRUE;
+    }
+    else {
+      unset($form['#disable_inline_form_errors']);
+      $disable_inline_form_errors = FALSE;
+    }
+
+    $form['accessibility']['disable_inline_form_errors'] = [
+      '#type' => 'link',
+      '#title' => $disable_inline_form_errors ? t('Enable inline form errors') : t('Disable inline form errors'),
+      '#url' => Url::fromRoute('<current>', [], ['query' => ['disable_inline_form_errors' => $disable_inline_form_errors ? 0 : 1] + $query]),
+    ];
+  }
+}
diff --git a/web/modules/webform/modules/webform_image_select/config/schema/webform_image_select.schema.yml b/web/modules/webform/modules/webform_image_select/config/schema/webform_image_select.schema.yml
index 3e0bab240e91b8e3819dd4376d4848d955fb0f7b..dc311cae5b76250915e7abed9966f08e127b2e58 100644
--- a/web/modules/webform/modules/webform_image_select/config/schema/webform_image_select.schema.yml
+++ b/web/modules/webform/modules/webform_image_select/config/schema/webform_image_select.schema.yml
@@ -1,16 +1,16 @@
-webform_image_select.webform_image_select_images.*:
+'webform_image_select.webform_image_select_images.*':
   type: config_entity
-  label: 'Images'
+  label: Images
   mapping:
     id:
       type: string
       label: 'Machine name'
     label:
       type: label
-      label: 'Label'
+      label: Label
     category:
       type: label
-      label: 'Category'
+      label: Category
     images:
       type: text
       label: 'Images (YAML)'
diff --git a/web/modules/webform/modules/webform_image_select/css/webform_image_select.element.css b/web/modules/webform/modules/webform_image_select/css/webform_image_select.element.css
index 5654f4555497d75a06d5b21e0652aed718d9a307..c171acfdc6b00c279cbbf30b30a63a36d7add152 100644
--- a/web/modules/webform/modules/webform_image_select/css/webform_image_select.element.css
+++ b/web/modules/webform/modules/webform_image_select/css/webform_image_select.element.css
@@ -14,6 +14,18 @@ html.js .js-webform-image-select {
   opacity: 0;
 }
 
+.webform-image-select-filter .field-suffix {
+  margin-left: 10px;
+}
+
 .thumbnails.image_picker_selector p {
   margin: 0;
 }
+
+.thumbnails.image_picker_selector .thumbnail.focused {
+  border: 1px solid #0088cc;
+}
+
+.thumbnails.image_picker_selector .thumbnail.selected {
+  color: #fff;
+}
diff --git a/web/modules/webform/modules/webform_image_select/js/webform_image_select.element.js b/web/modules/webform/modules/webform_image_select/js/webform_image_select.element.js
index 72eb938a8798f13bd4198075373294f24e0b8fd1..74d3e075919ff5f8cd015fb0049d62a42b1d492b 100644
--- a/web/modules/webform/modules/webform_image_select/js/webform_image_select.element.js
+++ b/web/modules/webform/modules/webform_image_select/js/webform_image_select.element.js
@@ -25,6 +25,7 @@
 
       $('.js-webform-image-select', context).once('webform-image-select').each(function () {
         var $select = $(this);
+        var isMultiple = $select.attr('multiple');
 
         // Apply image data to options.
         var images = JSON.parse($select.attr('data-images'));
@@ -50,6 +51,72 @@
         }
 
         $select.imagepicker(options);
+
+        // Add very basic accessibility to the image picker by
+        // enabling tabbing and toggling via the spacebar.
+        // @see https://github.com/rvera/image-picker/issues/108
+
+        // Block select menu from being tabbed.
+        $select.attr('tabindex', '-1');
+
+        if (isMultiple) {
+          $select.next('.image_picker_selector').attr('role', 'radiogroup');
+        }
+
+        var $thumbnail = $select.next('.image_picker_selector').find('.thumbnail');
+        $thumbnail
+          // Allow thumbnail to be tabbed.
+          .prop('tabindex', '0')
+          .attr('role', isMultiple ? 'checkbox' : 'radio')
+          .each(function () {
+            var alt = $(this).find('img').attr('alt');
+            // Cleanup alt, set title, and fix aria.
+            if (alt) {
+              alt = alt.replace(/<\/?[^>]+(>|$)/g, '');
+              $(this).find('img').attr('alt', alt);
+              $(this).attr('title', alt);
+            }
+
+            // Aria hide caption since the 'title' attribute will be read aloud.
+            $(this).find('p').attr('aria-hidden', true);
+          })
+          .on('focus', function (event) {
+            $(this).addClass('focused');
+          })
+          .on('blur', function (event) {
+            $(this).removeClass('focused');
+          })
+          .on('keydown', function (event) {
+            if (event.which === 32) {
+              // Space.
+              $(this).click();
+              event.preventDefault();
+            }
+            else if (event.which === 37 || event.which === 38) {
+              // Left or Up.
+              var $prev = $(this).parent();
+              do {
+                $prev = $prev.prev();
+              }
+              while ($prev.length && $prev.is(':hidden'));
+              $prev.find('.thumbnail').focus();
+              event.preventDefault();
+            }
+            else if (event.which === 39 || event.which === 40) {
+              // Right or Down.
+              var $next = $(this).parent();
+              do {
+                $next = $next.next();
+              }
+              while ($next.length && $next.is(':hidden'));
+              $next.find('.thumbnail').focus();
+              event.preventDefault();
+            }
+          })
+          .on('click', function (event) {
+            var selected = $(this).hasClass('selected');
+            $(this).attr('aria-checked', selected);
+          });
       });
     }
   };
diff --git a/web/modules/webform/modules/webform_image_select/src/Access/WebformImageSelectAccess.php b/web/modules/webform/modules/webform_image_select/src/Access/WebformImageSelectAccess.php
index a80ae4d3383d1acd25c796e4ad6a1ba29ab00fc6..108013aeecd8146d46cfc6a16d6eac102f4b4016 100644
--- a/web/modules/webform/modules/webform_image_select/src/Access/WebformImageSelectAccess.php
+++ b/web/modules/webform/modules/webform_image_select/src/Access/WebformImageSelectAccess.php
@@ -23,7 +23,8 @@ class WebformImageSelectAccess {
    *   The access result.
    */
   public static function checkImagesSourceAccess(WebformImageSelectImagesInterface $webform_image_select_images, AccountInterface $account) {
-    return AccessResult::allowedIf($webform_image_select_images->access('update', $account) && $account->hasPermission('edit webform source'));
+    return $webform_image_select_images->access('update', $account, TRUE)
+      ->andIf(AccessResult::allowedIfHasPermission($account, 'edit webform source'));
   }
 
 }
diff --git a/web/modules/webform/modules/webform_image_select/src/Controller/WebformImageSelectImagesController.php b/web/modules/webform/modules/webform_image_select/src/Controller/WebformImageSelectImagesController.php
new file mode 100644
index 0000000000000000000000000000000000000000..09dd3bc3af9b5c6676eb24da15d8cf443b70fb59
--- /dev/null
+++ b/web/modules/webform/modules/webform_image_select/src/Controller/WebformImageSelectImagesController.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\webform_image_select\Controller;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Controller\ControllerBase;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides route responses for webform images options.
+ */
+class WebformImageSelectImagesController extends ControllerBase {
+
+  /**
+   * Returns response for the webform options autocompletion.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request object containing the search string.
+   *
+   * @return \Symfony\Component\HttpFoundation\JsonResponse
+   *   A JSON response containing the autocomplete suggestions.
+   */
+  public function autocomplete(Request $request) {
+    $q = $request->query->get('q');
+
+    $webform_images_storage = $this->entityTypeManager()->getStorage('webform_image_select_images');
+
+    $query = $webform_images_storage->getQuery()
+      ->range(0, 10)
+      ->sort('label');
+
+    // Query title and id.
+    $or = $query->orConditionGroup()
+      ->condition('id', $q, 'CONTAINS')
+      ->condition('label', $q, 'CONTAINS');
+    $query->condition($or);
+
+    $entity_ids = $query->execute();
+
+    if (empty($entity_ids)) {
+      return new JsonResponse([]);
+    }
+    $webform_images = $webform_images_storage->loadMultiple($entity_ids);
+
+    $matches = [];
+    foreach ($webform_images as $webform_image) {
+      $value = new FormattableMarkup('@label (@id)', ['@label' => $webform_image->label(), '@id' => $webform_image->id()]);
+      $matches[] = ['value' => $value, 'label' => $value];
+    }
+
+    return new JsonResponse($matches);
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_image_select/src/Element/WebformImageSelect.php b/web/modules/webform/modules/webform_image_select/src/Element/WebformImageSelect.php
index cccda438e4340f559efc58a382f588e99bbef1fe..21267f07e55f7829922a79802769fa5ebb01ad80 100644
--- a/web/modules/webform/modules/webform_image_select/src/Element/WebformImageSelect.php
+++ b/web/modules/webform/modules/webform_image_select/src/Element/WebformImageSelect.php
@@ -3,10 +3,11 @@
 namespace Drupal\webform_image_select\Element;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element\Select;
-use Drupal\webform\Utility\WebformArrayHelper;
+use Drupal\webform\Utility\WebformElementHelper;
 
 /**
  * Provides a webform element for a selecting an image.
@@ -23,6 +24,11 @@ public function getInfo() {
     $info['#images'] = [];
     $info['#images_randomize'] = FALSE;
     $info['#show_label'] = FALSE;
+    $info['#filter'] = FALSE;
+    $info['#filter__placeholder'] = NULL;
+    $info['#filter__singular'] = NULL;
+    $info['#filter__plural'] = NULL;
+    $info['#filter__no_result'] = NULL;
     return $info;
   }
 
@@ -38,6 +44,67 @@ public static function processSelect(&$element, FormStateInterface $form_state,
       $element['#attributes']['data-show-label'] = 'data-show-label';
     }
 
+    // Add label filter.
+    if ($element['#show_label'] && $element['#filter']) {
+      $field_prefix = (isset($element['#field_prefix'])) ? $element['#field_prefix'] : NULL;
+
+      $wrapper_class = 'js-' . Html::getClass($element['#name'] . '-filter');
+      $element['#wrapper_attributes']['class'][] = $wrapper_class;
+      $singular = (!empty($element['#filter__singular'])) ? $element['#filter__singular'] : t('image');
+      $plural = (!empty($element['#filter__plural'])) ? $element['#filter__plural'] : t('images');
+      $count = count($element['#images']);
+      $element['#field_prefix'] = [
+        'filter' => [
+          '#type' => 'search',
+          '#id' => $element['#id'] . '-filter',
+          '#name' => $element['#name'] . '_filter',
+          '#title' => t('Filter'),
+          '#title_display' => 'invisible',
+          '#size' => 30,
+          '#placeholder' => (!empty($element['#filter__placeholder'])) ? $element['#filter__placeholder'] : t('Filter images by label'),
+          '#attributes' => [
+            'class' => ['webform-form-filter-text'],
+            'data-focus' => 'false',
+            'data-item-singlular' => $singular,
+            'data-item-plural' => $plural,
+            'data-summary' => ".$wrapper_class .webform-image-select-summary",
+            'data-no-results' => ".$wrapper_class .webform-image-select-no-results",
+            'data-element' => ".$wrapper_class .thumbnails",
+            'data-source' =>  ".thumbnail p",
+            'data-parent' => 'li',
+            'data-selected' => '.selected',
+            'title' => t('Enter a keyword to filter by.'),
+          ],
+          '#wrapper_attributes' => ['class' => ['webform-image-select-filter']],
+          '#field_suffix' => [
+            'info' => [
+              '#type' => 'html_tag',
+              '#tag' => 'span',
+              '#attributes' => ['class' => ['webform-image-select-summary']],
+              'content' => [
+                '#markup' => t('@count @items', [
+                  '@count' => $count,
+                  '@items' => ($count === 1) ? $singular : $plural,
+                ]),
+              ],
+            ],
+            'no_results' => [
+              '#type' => 'webform_message',
+              '#attributes' => ['style' => 'display:none', 'class' => ['webform-image-select-no-results']],
+              '#message_message' => (!empty($element['#filter__no_results'])) ? $element['#filter__no_results'] : t('No images found.'),
+              '#message_type' => 'info',
+            ],
+          ],
+        ],
+      ];
+
+      if ($field_prefix) {
+        $element['#field_prefix']['field_prefix'] = (is_array($element['#field_prefix']))
+          ? $element['#field_prefix']
+          : ['#markup' => $element['#field_prefix']];
+      }
+    }
+
     // Set limit.
     if ($element['#multiple'] && $element['#multiple'] > 1) {
       $element['#attributes']['data-limit'] = $element['#multiple'];
@@ -53,6 +120,9 @@ public static function processSelect(&$element, FormStateInterface $form_state,
 
     // Attach library.
     $element['#attached']['library'][] = 'webform_image_select/webform_image_select.element';
+    if ($element['#show_label'] && $element['#filter']) {
+      $element['#attached']['library'][] = 'webform/webform.filter';
+    }
 
     return parent::processSelect($element, $form_state, $complete_form);
   }
@@ -66,16 +136,21 @@ public static function processSelect(&$element, FormStateInterface $form_state,
   public static function setOptions(array &$element) {
     // Randomize images.
     if (!empty($element['#images_randomize'])) {
-      $element['#images'] = array_values(WebformArrayHelper::shuffle($element['#images']));
+      $element['#images'] = WebformElementHelper::randomize($element['#images']);
     }
 
     // Convert #images to #options and make sure images are keyed by value.
-    if (empty($element['#options']) && !empty($element['#images'])) {
+    if (empty($element['#options'])) {
       $options = [];
       foreach ($element['#images'] as $value => &$image) {
-        // Apply XSS filter to image text.
-        $image['text'] = Xss::filter($image['text']);
-        $options[$value] = $image['text'];
+        if (isset($image['text'])) {
+          // Apply XSS filter to image text.
+          $image['text'] = Xss::filter($image['text']);
+          $options[$value] = $image['text'];
+        }
+        else {
+          $options[$value] = $value;
+        }
       }
       $element['#options'] = $options;
     }
diff --git a/web/modules/webform/modules/webform_image_select/src/Element/WebformImageSelectElementImages.php b/web/modules/webform/modules/webform_image_select/src/Element/WebformImageSelectElementImages.php
index 23e3b7f2a54242166fc8d34dd94ed328b7edeb1a..7d98d3d6a8498570260ec649caecaa3f474f4cfa 100644
--- a/web/modules/webform/modules/webform_image_select/src/Element/WebformImageSelectElementImages.php
+++ b/web/modules/webform/modules/webform_image_select/src/Element/WebformImageSelectElementImages.php
@@ -85,7 +85,7 @@ public static function processWebformImageSelectElementImages(&$element, FormSta
       '#type' => 'select',
       '#description' => t('Please select <a href=":href">predefined images</a> or enter custom image.', $t_args),
       '#options' => [
-        self::CUSTOM_OPTION => t('Custom images...'),
+        self::CUSTOM_OPTION => t('Custom images…'),
       ] + $webform_images,
       '#attributes' => [
         'class' => [$class_name],
diff --git a/web/modules/webform/modules/webform_image_select/src/Element/WebformImageSelectImages.php b/web/modules/webform/modules/webform_image_select/src/Element/WebformImageSelectImages.php
index bb500036b533e17f0d2dd5971dc7970817976f5f..9e72c55ccc2b8d5474e33e1412be7be0d98ef7f5 100644
--- a/web/modules/webform/modules/webform_image_select/src/Element/WebformImageSelectImages.php
+++ b/web/modules/webform/modules/webform_image_select/src/Element/WebformImageSelectImages.php
@@ -24,8 +24,9 @@ public function getInfo() {
       '#input' => TRUE,
       '#label' => t('image'),
       '#labels' => t('images'),
-      '#empty_items' => 5,
-      '#add_more' => 1,
+      '#min_items' => 3,
+      '#empty_items' => 1,
+      '#add_more_items' => 1,
       '#process' => [
         [$class, 'processWebformImageSelectImages'],
       ],
@@ -63,7 +64,7 @@ public static function processWebformImageSelectImages(&$element, FormStateInter
     // Wrap this $element in a <div> that handle #states.
     WebformElementHelper::fixStatesWrapper($element);
 
-    $properties = ['#label', '#labels', '#empty_items', '#add_more'];
+    $properties = ['#label', '#labels', '#min_items', '#empty_items', '#add_more_items'];
 
     $element['images'] = array_intersect_key($element, array_combine($properties, $properties)) + [
       '#type' => 'webform_multiple',
@@ -77,26 +78,28 @@ public static function processWebformImageSelectImages(&$element, FormStateInter
         'value' => [
           '#type' => 'textfield',
           '#title' => t('Image value'),
-          '#title_display' => t('invisible'),
-          '#placeholder' => t('Enter value'),
+          '#title_display' => 'invisible',
+          '#placeholder' => t('Enter value…'),
           '#error_no_message' => TRUE,
+          '#attributes' => ['class' => ['js-webform-options-sync']],
         ],
         'text' => [
           '#type' => 'textfield',
           '#title' => t('Image text'),
-          '#title_display' => t('invisible'),
-          '#placeholder' => t('Enter text'),
+          '#title_display' => 'invisible',
+          '#placeholder' => t('Enter text…'),
           '#error_no_message' => TRUE,
         ],
         'src' => [
           '#type' => 'textfield',
           '#title' => t('Image src'),
-          '#title_display' => t('invisible'),
-          '#placeholder' => t('Enter image src'),
+          '#title_display' => 'invisible',
+          '#placeholder' => t('Enter image src…'),
           '#error_no_message' => TRUE,
         ],
       ],
       '#error_no_message' => TRUE,
+      '#add_more_input_label' => t('more images'),
       '#default_value' => (isset($element['#default_value'])) ? $element['#default_value'] : [],
     ];
 
@@ -116,6 +119,8 @@ public static function processWebformImageSelectImages(&$element, FormStateInter
       ];
     }
 
+    $element['#attached']['library'][] = 'webform/webform.element.options.admin';
+
     return $element;
   }
 
diff --git a/web/modules/webform/modules/webform_image_select/src/Entity/WebformImageSelectImages.php b/web/modules/webform/modules/webform_image_select/src/Entity/WebformImageSelectImages.php
index d65b87d8b0ab90d2bbfe126691a4c4834ff497e9..3950a6aaa24ad07d9aa8b01005cff61106a956ea 100644
--- a/web/modules/webform/modules/webform_image_select/src/Entity/WebformImageSelectImages.php
+++ b/web/modules/webform/modules/webform_image_select/src/Entity/WebformImageSelectImages.php
@@ -15,6 +15,13 @@
  * @ConfigEntityType(
  *   id = "webform_image_select_images",
  *   label = @Translation("Webform images"),
+ *   label_collection = @Translation("Images"),
+ *   label_singular = @Translation("images"),
+ *   label_plural = @Translation("images"),
+ *   label_count = @PluralTranslation(
+ *     singular = "@count images",
+ *     plural = "@count images",
+ *   ),
  *   handlers = {
  *     "storage" = "\Drupal\webform_image_select\WebformImageSelectImagesStorage",
  *     "access" = "Drupal\webform_image_select\WebformImageSelectImagesAccessControlHandler",
@@ -146,9 +153,8 @@ public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b)
    * {@inheritdoc}
    */
   public static function getElementImages(array &$element) {
-
     // If element already has #images return them.
-    if (is_array($element['#images'])) {
+    if (isset($element['#images']) && is_array($element['#images'])) {
       return $element['#images'];
     }
 
@@ -167,7 +173,7 @@ public static function getElementImages(array &$element) {
       $images = [];
     }
 
-    // Alter iamges using hook_webform_image_select_images_alter()
+    // Alter images using hook_webform_image_select_images_alter()
     // and/or hook_webform_image_select_images_WEBFORM_IMAGE_SELECT_IMAGES_ID_alter() hook.
     // @see webform.api.php
     \Drupal::moduleHandler()->alter('webform_image_select_images_' . $id, $images, $element);
diff --git a/web/modules/webform/modules/webform_image_select/src/Form/WebformImageSelectImagesFilterForm.php b/web/modules/webform/modules/webform_image_select/src/Form/WebformImageSelectImagesFilterForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..84ee7e24448039d6a7154b72ca01897d29a38330
--- /dev/null
+++ b/web/modules/webform/modules/webform_image_select/src/Form/WebformImageSelectImagesFilterForm.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Drupal\webform_image_select\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides the webform image select images filter form.
+ */
+class WebformImageSelectImagesFilterForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'webform_image_select_images_filter_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $search = NULL, $category = NULL, array $categories = []) {
+    $form['#attributes'] = ['class' => ['webform-filter-form']];
+    $form['filter'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Filter images'),
+      '#open' => TRUE,
+      '#attributes' => ['class' => ['container-inline']],
+    ];
+    $form['filter']['search'] = [
+      '#type' => 'search',
+      '#title' => $this->t('Keyword'),
+      '#title_display' => 'invisible',
+      '#autocomplete_route_name' => 'entity.webform_image_select_images.autocomplete',
+      '#placeholder' => $this->t('Filter by title or images'),
+      '#size' => 45,
+      '#default_value' => $search,
+    ];
+    $form['filter']['category'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Category'),
+      '#title_display' => 'invisible',
+      '#options' => $categories,
+      '#empty_option' => ($category) ? $this->t('Show all images') : $this->t('Filter by category'),
+      '#default_value' => $category,
+    ];
+    $form['filter']['submit'] = [
+      '#type' => 'submit',
+      '#button_type' => 'primary',
+      '#value' => $this->t('Filter'),
+    ];
+    if (!empty($search) || !empty($category)) {
+      $form['filter']['reset'] = [
+        '#type' => 'submit',
+        '#submit' => ['::resetForm'],
+        '#value' => $this->t('Reset'),
+      ];
+    }
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $query = [
+      'search' => trim($form_state->getValue('search')),
+      'category' => trim($form_state->getValue('category')),
+    ];
+    $form_state->setRedirect($this->getRouteMatch()->getRouteName(), $this->getRouteMatch()->getRawParameters()->all(), [
+      'query' => $query ,
+    ]);
+  }
+
+  /**
+   * Resets the filter selection.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function resetForm(array &$form, FormStateInterface $form_state) {
+    $form_state->setRedirect($this->getRouteMatch()->getRouteName(), $this->getRouteMatch()->getRawParameters()->all());
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_image_select/src/Plugin/WebformElement/WebformImageSelect.php b/web/modules/webform/modules/webform_image_select/src/Plugin/WebformElement/WebformImageSelect.php
index 20a40a0d67e3407daed2ea0e233c3ba8c9e41e38..731d1dab0f9d960a6fb9cdf7f42f7b26de477abe 100644
--- a/web/modules/webform/modules/webform_image_select/src/Plugin/WebformElement/WebformImageSelect.php
+++ b/web/modules/webform/modules/webform_image_select/src/Plugin/WebformElement/WebformImageSelect.php
@@ -38,6 +38,11 @@ public function getDefaultProperties() {
     $properties['images'] = [];
     $properties['images_randomize'] = FALSE;
     $properties['show_label'] = FALSE;
+    $properties['filter'] = FALSE;
+    $properties['filter__placeholder'] = $this->t('Filter images by label');
+    $properties['filter__singlular'] = $this->t('image');
+    $properties['filter__plural'] = $this->t('images');
+    $properties['filter__no_results'] = $this->t('No images found.');
     return $properties;
   }
 
@@ -45,7 +50,13 @@ public function getDefaultProperties() {
    * {@inheritdoc}
    */
   public function getTranslatableProperties() {
-    return array_merge(parent::getTranslatableProperties(), ['images']);
+    return array_merge(parent::getTranslatableProperties(), [
+      'images',
+      'filter__placeholder',
+      'filter__singlular',
+      'filter__plural',
+      'filter__no_results',
+    ]);
   }
 
   /**
@@ -53,9 +64,7 @@ public function getTranslatableProperties() {
    */
   public function initialize(array &$element) {
     // Set element images.
-    if (isset($element['#images'])) {
-      $element['#images'] = WebformImageSelectImages::getElementImages($element);
-    }
+    $element['#images'] = WebformImageSelectImages::getElementImages($element);
 
     WebformImageSelectElement::setOptions($element);
 
@@ -228,6 +237,41 @@ public function form(array $form, FormStateInterface $form_state) {
       '#description' => $this->t('If checked, the image text will be displayed below each image.'),
       '#return_value' => TRUE,
     ];
+    $form['options']['filter'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Include filter by label'),
+      '#description' => $this->t('If checked, users will be able search/filter images by their labels.'),
+      '#return_value' => TRUE,
+      '#states' => [
+        'visible' => [
+          ':input[name="properties[show_label]"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    $form['options']['filter_container'] = [
+      '#type' => 'container',
+      '#states' => [
+        'visible' => [
+          ':input[name="properties[filter]"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    $form['options']['filter_container']['filter__placeholder'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Filter placeholder label'),
+    ];
+    $form['options']['filter_container']['filter__singlular'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Filter single item label'),
+    ];
+    $form['options']['filter_container']['filter__plural'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Filter plural items label'),
+    ];
+    $form['options']['filter_container']['filter__no_results'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Filter no results label'),
+    ];
     return $form;
   }
 
diff --git a/web/modules/webform/modules/webform_image_select/src/Tests/WebformImageSelectElementImagesTest.php b/web/modules/webform/modules/webform_image_select/src/Tests/WebformImageSelectElementImagesTest.php
index 1c1211ff3396127c23d54a2b24aa3b7ceb8ee45f..0e5d2b897969cbb0dccb7b85d3ee0d7df5d6c6a7 100644
--- a/web/modules/webform/modules/webform_image_select/src/Tests/WebformImageSelectElementImagesTest.php
+++ b/web/modules/webform/modules/webform_image_select/src/Tests/WebformImageSelectElementImagesTest.php
@@ -52,6 +52,14 @@ public function testElementOptions() {
   kitten_4:
     text: 'Cute Kitten 4'
     src: 'http://placekitten.com/270/200'");
+
+    // Check unique key validation with image src.
+    $edit = [
+      'webform_image_select_images[images][items][0][src]' => 'src01',
+      'webform_image_select_images[images][items][1][src]' => 'src02',
+    ];
+    $this->drupalPostForm('webform/test_element_images', $edit, t('Submit'));
+    $this->assertRaw("The <em class=\"placeholder\">Image value</em> '' is already in use. It must be unique.");
   }
 
 }
diff --git a/web/modules/webform/modules/webform_image_select/src/Tests/WebformImageSelectElementTest.php b/web/modules/webform/modules/webform_image_select/src/Tests/WebformImageSelectElementTest.php
index 1236f6de72c0bda862ed7f2e702baafe7deb1e6a..656373a514df5b573499f6ba4b986476a114faca 100644
--- a/web/modules/webform/modules/webform_image_select/src/Tests/WebformImageSelectElementTest.php
+++ b/web/modules/webform/modules/webform_image_select/src/Tests/WebformImageSelectElementTest.php
@@ -23,16 +23,22 @@ class WebformImageSelectElementTest extends WebformElementTestBase {
    * Test webform image select element.
    */
   public function testImageSelect() {
-    $this->drupalGet('webform/test_element_image_select');
+    $this->drupalGet('/webform/test_element_image_select');
 
     // Check rendering of image select with required.
     $this->assertRaw('<select data-drupal-selector="edit-image-select-default" data-images="{&quot;kitten_1&quot;:{&quot;text&quot;:&quot;Cute Kitten 1&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/220\/200&quot;},&quot;kitten_2&quot;:{&quot;text&quot;:&quot;Cute Kitten 2&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/180\/200&quot;},&quot;kitten_3&quot;:{&quot;text&quot;:&quot;Cute Kitten 3&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/130\/200&quot;},&quot;kitten_4&quot;:{&quot;text&quot;:&quot;Cute Kitten 4&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/270\/200&quot;}}" class="webform-image-select js-webform-image-select form-select required" id="edit-image-select-default" name="image_select_default" required="required" aria-required="true">');
 
     // Check rendering of image select with limit.
-    $this->assertRaW('<select data-drupal-selector="edit-image-select-limit" data-limit="2" data-images="{&quot;kitten_1&quot;:{&quot;text&quot;:&quot;Cute Kitten 1&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/220\/200&quot;},&quot;kitten_2&quot;:{&quot;text&quot;:&quot;Cute Kitten 2&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/180\/200&quot;},&quot;kitten_3&quot;:{&quot;text&quot;:&quot;Cute Kitten 3&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/130\/200&quot;},&quot;kitten_4&quot;:{&quot;text&quot;:&quot;Cute Kitten 4&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/270\/200&quot;}}" class="webform-image-select js-webform-image-select form-select" multiple="multiple" name="image_select_limit[]" id="edit-image-select-limit">');
+    $this->assertRaw('<select data-drupal-selector="edit-image-select-limit" data-limit="2" data-images="{&quot;kitten_1&quot;:{&quot;text&quot;:&quot;Cute Kitten 1&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/220\/200&quot;},&quot;kitten_2&quot;:{&quot;text&quot;:&quot;Cute Kitten 2&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/180\/200&quot;},&quot;kitten_3&quot;:{&quot;text&quot;:&quot;Cute Kitten 3&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/130\/200&quot;},&quot;kitten_4&quot;:{&quot;text&quot;:&quot;Cute Kitten 4&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/270\/200&quot;}}" class="webform-image-select js-webform-image-select form-select" multiple="multiple" name="image_select_limit[]" id="edit-image-select-limit">');
 
     // Check rendering of image select with HTML markup and XSS test.
-    $this->assertRaW('<select data-drupal-selector="edit-image-select-html" data-show-label="data-show-label" data-images="{&quot;\u003C1\u003E&quot;:{&quot;text&quot;:&quot;Cute Kitten 1&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/220\/200&quot;},&quot;\u00222\u0022&quot;:{&quot;text&quot;:&quot;Cute \u003Cem\u003EKitten\u003C\/em\u003E 2&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/180\/200&quot;},&quot;\u00263&quot;:{&quot;text&quot;:&quot;Cute Kitten 3&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/130\/200&quot;},&quot;4&quot;:{&quot;text&quot;:&quot;Cute Kitten 4 alert(\u0022XSS\u0022);&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/270\/200&quot;}}" class="webform-image-select js-webform-image-select form-select" id="edit-image-select-html" name="image_select_html"><option value="" selected="selected">- None -</option><option value="&lt;1&gt;">');
+    $this->assertRaw('<select data-drupal-selector="edit-image-select-html" data-show-label="data-show-label" data-images="{&quot;\u003C1\u003E&quot;:{&quot;text&quot;:&quot;Cute Kitten 1&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/220\/200&quot;},&quot;\u00222\u0022&quot;:{&quot;text&quot;:&quot;Cute \u003Cem\u003EKitten\u003C\/em\u003E 2&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/180\/200&quot;},&quot;\u00263&quot;:{&quot;text&quot;:&quot;Cute Kitten 3&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/130\/200&quot;},&quot;4&quot;:{&quot;text&quot;:&quot;Cute Kitten 4 alert(\u0022XSS\u0022);&quot;,&quot;src&quot;:&quot;http:\/\/placekitten.com\/270\/200&quot;}}" class="webform-image-select js-webform-image-select form-select" id="edit-image-select-html" name="image_select_html"><option value="" selected="selected">- None -</option><option value="&lt;1&gt;">');
+
+    // Check rendering with filter.
+    $this->assertRaw('<input class="webform-form-filter-text form-search" data-focus="false" data-item-singlular="animal" data-item-plural="animals" data-summary=".js-image-select-filter-custom-filter .webform-image-select-summary" data-no-results=".js-image-select-filter-custom-filter .webform-image-select-no-results" data-element=".js-image-select-filter-custom-filter .thumbnails" data-source=".thumbnail p" data-parent="li" data-selected=".selected" title="Enter a keyword to filter by." type="search" id="edit-image-select-filter-custom-filter" name="image_select_filter_custom_filter" size="30" maxlength="128" placeholder="Find an animal" />');
+    $this->assertRaw('<span class="field-suffix"><span class="webform-image-select-summary">8 animals</span>');
+    $this->assertRaw('<div style="display:none" class="webform-image-select-no-results webform-message js-webform-message">');
+    $this->assertRaw('No animals found.');
 
     // Check preview.
     $webform = Webform::load('test_element_image_select');
diff --git a/web/modules/webform/modules/webform_image_select/src/Tests/WebformImageSelectImagesTest.php b/web/modules/webform/modules/webform_image_select/src/Tests/WebformImageSelectImagesTest.php
index da175eed7834ef7d0223ca397d81cf1f03e83ead..39ffb5ea405f45f3aa1ad4528a6e53a9367bd4f7 100644
--- a/web/modules/webform/modules/webform_image_select/src/Tests/WebformImageSelectImagesTest.php
+++ b/web/modules/webform/modules/webform_image_select/src/Tests/WebformImageSelectImagesTest.php
@@ -21,21 +21,19 @@ class WebformImageSelectImagesTest extends WebformTestBase {
    */
   public static $modules = ['webform_image_select', 'webform_image_select_test'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
   /**
    * Tests webform image select images entity.
    */
   public function testWebformImageSelectImages() {
-    $this->drupalLogin($this->normalUser);
+    $normal_user = $this->drupalCreateUser();
+
+    $admin_user = $this->drupalCreateUser([
+      'administer webform',
+    ]);
+
+    /**************************************************************************/
+
+    $this->drupalLogin($normal_user);
 
     // Check get element images.
     $kittens = Yaml::decode("kitten_1:
@@ -94,22 +92,22 @@ public function testWebformImageSelectImages() {
     $this->assertFalse($webform_images->getImages());
 
     // Check admin user access denied.
-    $this->drupalGet('admin/structure/webform/config/images/manage');
+    $this->drupalGet('/admin/structure/webform/config/images/manage');
     $this->assertResponse(403);
-    $this->drupalGet('admin/structure/webform/config/images/manage/add');
+    $this->drupalGet('/admin/structure/webform/config/images/manage/add');
     $this->assertResponse(403);
-    $this->drupalGet('admin/structure/webform/config/images/manage/animals/edit');
+    $this->drupalGet('/admin/structure/webform/config/images/manage/animals/edit');
     $this->assertResponse(403);
 
     // Check admin user access.
-    $this->drupalLogin($this->adminWebformUser);
-    $this->drupalGet('admin/structure/webform/config/images/manage');
+    $this->drupalLogin($admin_user);
+    $this->drupalGet('/admin/structure/webform/config/images/manage');
     $this->assertResponse(200);
-    $this->drupalGet('admin/structure/webform/config/images/manage/add');
+    $this->drupalGet('/admin/structure/webform/config/images/manage/add');
     $this->assertResponse(200);
 
     // Check image altered message.
-    $this->drupalGet('admin/structure/webform/config/images/manage/animals/edit');
+    $this->drupalGet('/admin/structure/webform/config/images/manage/animals/edit');
     $this->assertRaw('The <em class="placeholder">Cute Animals</em> images are being altered by the <em class="placeholder">Webform Image Select Test</em> module.');
 
     // Check hook_webform_image_select_images_alter().
diff --git a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesAccessControlHandler.php b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesAccessControlHandler.php
index a88c6b228064589f5ae871698b60b3e6b243dedb..17395f29dd37980a3441228c5790835bc845bf54 100644
--- a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesAccessControlHandler.php
+++ b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesAccessControlHandler.php
@@ -18,7 +18,7 @@ class WebformImageSelectImagesAccessControlHandler extends EntityAccessControlHa
    * {@inheritdoc}
    */
   public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
-    return AccessResult::allowedIf($account->hasPermission('administer webform'))->cachePerPermissions();
+    return AccessResult::allowedIfHasPermission($account, 'administer webform');
   }
 
 }
diff --git a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesDeleteForm.php b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesDeleteForm.php
index 85458cc39850974fdf321abe48a2303c60d3b41c..b777008bb485d60b76c34624953cb2365c1b6d0d 100644
--- a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesDeleteForm.php
+++ b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesDeleteForm.php
@@ -2,60 +2,68 @@
 
 namespace Drupal\webform_image_select;
 
-use Drupal\Core\Entity\EntityDeleteForm;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Url;
-use Drupal\webform\Form\WebformDialogFormTrait;
+use Drupal\webform\Form\WebformConfigEntityDeleteFormBase;
 
 /**
  * Provides a delete webform images select images form.
  */
-class WebformImageSelectImagesDeleteForm extends EntityDeleteForm {
-
-  use WebformDialogFormTrait;
+class WebformImageSelectImagesDeleteForm extends WebformConfigEntityDeleteFormBase {
 
   /**
    * {@inheritdoc}
    */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $form = parent::buildForm($form, $form_state);
+  public function getDescription() {
+    return [
+      'title' => [
+        '#markup' => $this->t('This action will…'),
+      ],
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => [
+          $this->t('Remove configuration'),
+          $this->t('Affect any elements which use these images'),
+        ],
+      ],
+    ];
+  }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getDetails() {
     /** @var \Drupal\webform\WebformOptionsInterface $webform_options */
     $webform_options = $this->entity;
 
     /** @var \Drupal\webform_image_select\WebformImageSelectImagesStorageInterface $webform_images_storage */
     $webform_images_storage = $this->entityTypeManager->getStorage('webform_image_select_images');
 
-    // Display warning that options is used by webforms.
-    $t_args = ['%title' => $webform_options->label()];
+    $t_args = [
+      '%label' => $this->getEntity()->label(),
+      '@entity-type' => $this->getEntity()->getEntityType()->getLowercaseLabel(),
+    ];
+
+    $details = [];
     if ($used_by_webforms = $webform_images_storage->getUsedByWebforms($webform_options)) {
-      $form['used_by_composite_elements'] = [
-        '#type' => 'webform_message',
-        '#message_message' => [
+      $details['used_by_composite_elements'] = [
+        'title' => [
+          '#markup' => $this->t('%label is used by the below webform(s).', $t_args),
+        ],
+        'list' => [
           '#theme' => 'item_list',
-          '#title' => $this->t('%title is used by the below webform(s).', $t_args),
           '#items' => $used_by_webforms,
         ],
-        '#message_type' => 'warning',
-        '#weight' => -100,
       ];
     }
 
-    $form['confirm'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Yes, I want to delete these webform images.'),
-      '#required' => TRUE,
-      '#weight' => 10,
-    ];
-
-    return $this->buildDialogConfirmForm($form, $form_state);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getRedirectUrl() {
-    return Url::fromRoute('entity.webform_image_select_images.collection');
+    if ($details) {
+      return [
+        '#type' => 'details',
+        '#title' => $this->t('Webforms affected'),
+      ] + $details;
+    }
+    else {
+      return [];
+    }
   }
 
 }
diff --git a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesForm.php b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesForm.php
index 8cfd58b4539234e88e3a3e58fda048d8a9b6ab8a..276ce39cb1e0001c3aceffcc1ef6925d919deb07 100644
--- a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesForm.php
+++ b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesForm.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
+use Drupal\webform\Utility\WebformDialogHelper;
 use Drupal\webform\Utility\WebformOptionsHelper;
 use Drupal\webform\Utility\WebformArrayHelper;
 
@@ -99,7 +100,7 @@ public function form(array $form, FormStateInterface $form_state) {
         '#title' => $this->t('Images'),
         '#title_display' => 'invisible',
         '#empty_options' => 10,
-        '#add_more' => 10,
+        '#add_more_items' => 10,
         '#default_value' => $this->getImages(),
       ];
     }
@@ -120,13 +121,28 @@ public function form(array $form, FormStateInterface $form_state) {
           '%module_names' => WebformArrayHelper::toString($module_names),
           '@module' => new PluralTranslatableMarkup(count($module_names), $this->t('module'), $this->t('modules')),
         ];
-        drupal_set_message($this->t('The %title images are being altered by the %module_names @module.', $t_args), 'warning');
+        $this->messenger()->addWarning($this->t('The %title images are being altered by the %module_names @module.', $t_args));
       }
     }
 
     return parent::form($form, $form_state);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, FormStateInterface $form_state) {
+    $actions = parent::actions($form, $form_state);
+
+    // Open delete button in a modal dialog.
+    if (isset($actions['delete'])) {
+      $actions['delete']['#attributes'] = WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW, $actions['delete']['#attributes']['class']);
+      WebformDialogHelper::attachLibraries($actions['delete']);
+    }
+
+    return $actions;
+  }
+
   /**
    * Get options.
    *
@@ -154,7 +170,9 @@ public function save(array $form, FormStateInterface $form_state) {
     ];
     $this->logger('webform_image_select')->notice('Images @label saved.', $context);
 
-    drupal_set_message($this->t('Images %label saved.', ['%label' => $images->label()]));
+    $this->messenger()->addStatus($this->t('Images %label saved.', [
+      '%label' => $images->label(),
+    ]));
 
     $form_state->setRedirect('entity.webform_image_select_images.collection');
   }
diff --git a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesInterface.php b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesInterface.php
index d42e17ed5e39c6424aacc505f243cb277f9904f7..2f4426889c2b6a6c30b3836767fc5d1d48fbbbc3 100644
--- a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesInterface.php
+++ b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesInterface.php
@@ -34,7 +34,7 @@ public function getImages();
    *   A webform image select element.
    *
    * @return array
-   *   An associative array of iamge.
+   *   An associative array of images.
    */
   public static function getElementImages(array &$element);
 
diff --git a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesListBuilder.php b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesListBuilder.php
index d554a7ca4488445c78df1adeb7f400b40c0aed4a..a43dc187f61e94000fd45896df6ae5fdf4a441cf 100644
--- a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesListBuilder.php
+++ b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesListBuilder.php
@@ -4,9 +4,14 @@
 
 use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Url;
 use Drupal\webform\Utility\WebformDialogHelper;
 use Drupal\webform_image_select\Entity\WebformImageSelectImages;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Defines a class to build a listing of webform image select images entities.
@@ -15,29 +20,109 @@
  */
 class WebformImageSelectImagesListBuilder extends ConfigEntityListBuilder {
 
+  /**
+   * Search keys.
+   *
+   * @var string
+   */
+  protected $keys;
+
+  /**
+   * Search category.
+   *
+   * @var string
+   */
+  protected $category;
+
+  /**
+   * Constructs a new WebformImageSelectImagesListBuilder object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   *   The entity storage class.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   */
+  public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, RequestStack $request_stack) {
+    parent::__construct($entity_type, $storage);
+    $this->request = $request_stack->getCurrentRequest();
+
+    $this->keys = $this->request->query->get('search');
+    $this->category = $this->request->query->get('category');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('entity.manager')->getStorage($entity_type->id()),
+      $container->get('request_stack')
+    );
+  }
+
   /**
    * {@inheritdoc}
    */
   public function render() {
+    // Handler autocomplete redirect.
+    if ($this->keys && preg_match('#\(([^)]+)\)$#', $this->keys, $match)) {
+      if ($webform_images = $this->getStorage()->load($match[1])) {
+        return new RedirectResponse($webform_images->toUrl()->setAbsolute(TRUE)->toString());
+      }
+    }
+
     $build = [];
 
+    // Filter form.
+    $build['filter_form'] = $this->buildFilterForm();
+
     // Display info.
-    if ($total = $this->getStorage()->getQuery()->count()->execute()) {
-      $build['info'] = [
-        '#markup' => $this->formatPlural($total, '@total images', '@total images', ['@total' => $total]),
-        '#prefix' => '<div>',
-        '#suffix' => '</div>',
-      ];
-    }
+    $build['info'] = $this->buildInfo();
 
+    // Table.
     $build += parent::render();
+    $build['table']['#sticky'] = TRUE;
 
+    // Attachments.
     $build['#attached']['library'][] = 'webform/webform.tooltip';
     $build['#attached']['library'][] = 'webform/webform.admin.dialog';
 
     return $build;
   }
 
+  /**
+   * Build the filter form.
+   *
+   * @return array
+   *   A render array representing the filter form.
+   */
+  protected function buildFilterForm() {
+    $categories = $this->getStorage()->getCategories();
+    return \Drupal::formBuilder()->getForm('\Drupal\webform_image_select\Form\WebformImageSelectImagesFilterForm', $this->keys, $this->category, $categories);
+  }
+
+  /**
+   * Build information summary.
+   *
+   * @return array
+   *   A render array representing the information summary.
+   */
+  protected function buildInfo() {
+    $total = $this->getQuery($this->keys, $this->category)->count()->execute();
+    if (!$total) {
+      return [];
+    }
+
+    return [
+      '#markup' => $this->formatPlural($total, '@total images', '@total images', ['@total' => $total]),
+      '#prefix' => '<div>',
+      '#suffix' => '</div>',
+    ];
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -88,13 +173,13 @@ public function getDefaultOperations(EntityInterface $entity, $type = 'edit') {
   /**
    * Build images for a webform image select images entity.
    *
-   * @param \Drupal\webform_image_select\WebformImageSelectImagesInterface $webform_images
+   * @param \Drupal\webform_image_select\WebformImageSelectImagesInterface $entity
    *   A webform image select images entity.
    *
    * @return array
    *   Images for a webform image select images entity.
    */
-  protected function buildImages(EntityInterface $entity) {
+  protected function buildImages(WebformImageSelectImagesInterface $entity) {
     $element = ['#images' => $entity->id()];
     $images = WebformImageSelectImages::getElementImages($element);
     if (!$images) {
@@ -137,7 +222,7 @@ protected function buildUsedBy(WebformImageSelectImagesInterface $webform_images
         '#type' => 'link',
         '#title' => $title,
         '#url' => Url::fromRoute('entity.webform.canonical', ['webform' => $id]),
-        '#suffix' => '</br>'
+        '#suffix' => '</br>',
       ];
     }
     return [
@@ -146,4 +231,56 @@ protected function buildUsedBy(WebformImageSelectImagesInterface $webform_images
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOperations(EntityInterface $entity) {
+    return parent::buildOperations($entity) + [
+      '#prefix' => '<div class="webform-dropbutton">',
+      '#suffix' => '</div>',
+    ];
+  }
+
+  /**
+   * Get the base entity query filtered by search and category.
+   *
+   * @param string $keys
+   *   (optional) Search key.
+   * @param string $category
+   *   (optional) Category.
+   *
+   * @return \Drupal\Core\Entity\Query\QueryInterface
+   *   An entity query.
+   */
+  protected function getQuery($keys = '', $category = '') {
+    $query = $this->getStorage()->getQuery();
+
+    // Filter by key(word).
+    if ($keys) {
+      $or = $query->orConditionGroup()
+        ->condition('id', $keys, 'CONTAINS')
+        ->condition('title', $keys, 'CONTAINS')
+        ->condition('images', $keys, 'CONTAINS');
+      $query->condition($or);
+    }
+
+    // Filter by category.
+    if ($category) {
+      $query->condition('category', $category);
+    }
+
+    return $query;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntityIds() {
+    $header = $this->buildHeader();
+    $query = $this->getQuery($this->keys, $this->category);
+    $query->tableSort($header);
+    $query->pager($this->limit);
+    return $query->execute();
+  }
+
 }
diff --git a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesStorage.php b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesStorage.php
index 6295a4a7b490e13e74ac9d11cb85065d6d029618..3697288d86e01620070d44efdefd27510df580f1 100644
--- a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesStorage.php
+++ b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesStorage.php
@@ -2,8 +2,8 @@
 
 namespace Drupal\webform_image_select;
 
-use Drupal\Component\Serialization\Yaml;
 use Drupal\Core\Config\Entity\ConfigEntityStorage;
+use Drupal\Core\Serialization\Yaml;
 
 /**
  * Storage controller class for "webform_image_select_images" configuration entities.
@@ -64,8 +64,8 @@ public function getUsedByWebforms(WebformImageSelectImagesInterface $webform_ima
         $config = $this->configFactory->get($webform_config_name);
         $element_data = Yaml::encode($config->get('elements'));
         if (preg_match_all('/images\'\: ([a-z_]+)/', $element_data, $matches)) {
-          $webform_id  = $config->get('id');
-          $webform_title  = $config->get('title');
+          $webform_id = $config->get('id');
+          $webform_title = $config->get('title');
           foreach ($matches[1] as $options_id) {
             $this->usedByWebforms[$options_id][$webform_id] = $webform_title;
           }
diff --git a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesStorageInterface.php b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesStorageInterface.php
index 5675d4b9b10a145b658a89d237efc7ab8e97580f..f4c910875e90119960478a2e0bd3296b127c85b7 100644
--- a/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesStorageInterface.php
+++ b/web/modules/webform/modules/webform_image_select/src/WebformImageSelectImagesStorageInterface.php
@@ -36,4 +36,5 @@ public function getImages();
    *   A list of webform that use the specified webform images.
    */
   public function getUsedByWebforms(WebformImageSelectImagesInterface $webform_images);
+
 }
diff --git a/web/modules/webform/modules/webform_image_select/tests/modules/webform_image_select_test/config/install/webform.webform.test_element_image_select.yml b/web/modules/webform/modules/webform_image_select/tests/modules/webform_image_select_test/config/install/webform.webform.test_element_image_select.yml
index 253db8c0a49062fa4a055cbad8036a5d8c755dcb..180f502a4d7ecb6e4d4e647ecaf81c65034639ce 100644
--- a/web/modules/webform/modules/webform_image_select/tests/modules/webform_image_select_test/config/install/webform.webform.test_element_image_select.yml
+++ b/web/modules/webform/modules/webform_image_select/tests/modules/webform_image_select_test/config/install/webform.webform.test_element_image_select.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_image_select_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_image_select
 title: 'Test: Element: Image'
 description: 'Test Image select element.'
@@ -30,6 +32,23 @@ elements: |
         text: 'Cute Kitten 4'
         src: 'http://placekitten.com/270/200'
     '#required': true
+  image_select_randomize:
+    '#type': webform_image_select
+    '#title': image_select_randomize
+    '#images':
+      kitten_1:
+        text: 'Cute Kitten 1'
+        src: 'http://placekitten.com/220/200'
+      kitten_2:
+        text: 'Cute Kitten 2'
+        src: 'http://placekitten.com/180/200'
+      kitten_3:
+        text: 'Cute Kitten 3'
+        src: 'http://placekitten.com/130/200'
+      kitten_4:
+        text: 'Cute Kitten 4'
+        src: 'http://placekitten.com/270/200'
+    '#images_randomize': true
   image_select_multiple:
     '#type': webform_image_select
     '#title': image_select_multiple
@@ -127,6 +146,23 @@ elements: |
     '#type': webform_image_select
     '#title': image_select_animals
     '#images': animals
+  image_select_filter:
+    '#type': webform_image_select
+    '#title': image_select_filter
+    '#images': animals
+    '#show_label': true
+    '#filter': true
+  image_select_filter_custom:
+    '#type': webform_image_select
+    '#title': image_select_filter_custom
+    '#multiple': true
+    '#images': animals
+    '#show_label': true
+    '#filter': true
+    '#filter__placeholder': 'Find an animal'
+    '#filter__singular': 'animal'
+    '#filter__plural': 'animals'
+    '#filter__no_results': 'No animals found.'
 css: ''
 javascript: ''
 settings:
@@ -135,6 +171,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -142,6 +179,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -157,22 +195,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -183,6 +236,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -196,12 +250,16 @@ settings:
   confirmation_back: true
   confirmation_back_label: ''
   confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -250,4 +308,12 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_image_select/tests/modules/webform_image_select_test/config/install/webform.webform.test_element_images.yml b/web/modules/webform/modules/webform_image_select/tests/modules/webform_image_select_test/config/install/webform.webform.test_element_images.yml
index 65313979d590be77e9271cf99648c0e92778d1fd..ea5c508b552ee67c85c47ed45703f547e9c889e0 100644
--- a/web/modules/webform/modules/webform_image_select/tests/modules/webform_image_select_test/config/install/webform.webform.test_element_images.yml
+++ b/web/modules/webform/modules/webform_image_select/tests/modules/webform_image_select_test/config/install/webform.webform.test_element_images.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_image_select_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_images
 title: 'Test: Element: Images'
 description: 'Test webform image select images element.'
@@ -42,12 +44,12 @@ elements: |
     '#open': true
     webform_image_select_element_images_entity:
       '#type': webform_image_select_element_images
-      '#title': 'webform_image_select_element_images_entity'
+      '#title': webform_image_select_element_images_entity
       '#required': true
       '#default_value': kittens
     webform_image_select_element_images_custom:
       '#type': webform_image_select_element_images
-      '#title': 'webform_image_select_element_images_custom'
+      '#title': webform_image_select_element_images_custom
       '#required': true
       '#default_value':
         kitten_1:
@@ -70,6 +72,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -77,6 +80,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -92,22 +96,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -118,6 +137,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -131,12 +151,16 @@ settings:
   confirmation_back: true
   confirmation_back_label: ''
   confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -185,6 +209,14 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/modules/webform_image_select/tests/modules/webform_image_select_test/webform_image_select_test.info.yml b/web/modules/webform/modules/webform_image_select/tests/modules/webform_image_select_test/webform_image_select_test.info.yml
index 5516da677bb54dd7b242919ef4086ad63bea4cd4..c31de6c23f0cab14aeed86d4946cb37d54131918 100644
--- a/web/modules/webform/modules/webform_image_select/tests/modules/webform_image_select_test/webform_image_select_test.info.yml
+++ b/web/modules/webform/modules/webform_image_select/tests/modules/webform_image_select_test/webform_image_select_test.info.yml
@@ -6,8 +6,8 @@ package: Testing
 dependencies:
   - 'webform_image_select:webform_image_select'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_image_select/webform_image_select.info.yml b/web/modules/webform/modules/webform_image_select/webform_image_select.info.yml
index 9965338da15e6ac29d7427dafd4d572c00ae32fa..31bb622e9d9d438df0ad049ec705606361804a07 100644
--- a/web/modules/webform/modules/webform_image_select/webform_image_select.info.yml
+++ b/web/modules/webform/modules/webform_image_select/webform_image_select.info.yml
@@ -6,8 +6,8 @@ package: Webform
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_image_select/webform_image_select.module b/web/modules/webform/modules/webform_image_select/webform_image_select.module
index 8506331900cd747d0c1bdef604e5527dba50476f..cdec386dd60f9b2626da925b2889382be9e939c8 100644
--- a/web/modules/webform/modules/webform_image_select/webform_image_select.module
+++ b/web/modules/webform/modules/webform_image_select/webform_image_select.module
@@ -6,6 +6,9 @@
  */
 
 use Drupal\Core\Url;
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Serialization\Yaml;
+use Drupal\Core\Form\FormStateInterface;
 
 /**
  * Implements hook_webform_libraries_info().
@@ -24,3 +27,57 @@ function webform_image_select_webform_libraries_info() {
   ];
   return $libraries;
 }
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ */
+function webform_image_select_menu_local_tasks_alter(&$data, $route_name) {
+  // Change config entities 'Translate *' tab to be just label 'Translate'.
+  if (isset($data['tabs'][0]["config_translation.local_tasks:entity.webform_image_select_images.config_translation_overview"]['#link']['title'])) {
+    $data['tabs'][0]["config_translation.local_tasks:entity.webform_image_select_images.config_translation_overview"]['#link']['title'] = t('Translate');
+  }
+}
+
+/******************************************************************************/
+// Lingotek integration.
+/******************************************************************************/
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function webform_image_select_form_config_translation_add_form_alter(&$form, FormStateInterface $form_state, $is_new = TRUE) {
+  // Manually apply YAML editor to text field that store YAML data.
+  // @see webform_form_config_translation_add_form_alter()
+  // @see _webform_form_config_translate_add_form_alter_yaml_element()
+  foreach ($form['config_names'] as $config_name => &$config_element) {
+    if (strpos($config_name, 'webform_image_select.webform_image_select_images.') === 0) {
+      _webform_form_config_translate_add_form_alter_yaml_element($config_element['images']);
+    }
+  }
+}
+
+/**
+ * Implements hook_lingotek_config_entity_document_upload().
+ */
+function webform_image_select_lingotek_config_entity_document_upload(array &$source_data, ConfigEntityInterface &$entity, &$url) {
+  switch ($entity->getEntityTypeId()) {
+    case 'webform_image_select_images';
+      // Convert images YAML string to an associative array.
+      $source_data['images'] = Yaml::decode($source_data['images']);
+      break;
+  }
+}
+
+/**
+ * Implements hook_lingotek_config_entity_translation_presave().
+ */
+function webform_image_select_lingotek_config_entity_translation_presave(ConfigEntityInterface &$translation, $langcode, &$data) {
+  switch ($translation->getEntityTypeId()) {
+    case 'webform_image_select_images';
+      /** @var \Drupal\webform_image_select\WebformImageSelectImagesInterface $translation */
+      // Convert images associative array back to YAML string.
+      $translation->setImages($data['images']);
+      $data['images'] = Yaml::encode($data['images']);
+      break;
+  }
+}
diff --git a/web/modules/webform/modules/webform_image_select/webform_image_select.routing.yml b/web/modules/webform/modules/webform_image_select/webform_image_select.routing.yml
index 6f092a709ef2a67c36c114ae4d5b99b9c188e5c4..cccb2ea29b6ac35033d581c57b0d843960d50234 100644
--- a/web/modules/webform/modules/webform_image_select/webform_image_select.routing.yml
+++ b/web/modules/webform/modules/webform_image_select/webform_image_select.routing.yml
@@ -6,6 +6,13 @@ entity.webform_image_select_images.collection:
   requirements:
     _permission: 'administer webform'
 
+entity.webform_image_select_images.autocomplete:
+  path: '/admin/structure/webform/config/images/autocomplete'
+  defaults:
+    _controller: '\Drupal\webform_image_select\Controller\WebformImageSelectImagesController::autocomplete'
+  requirements:
+    _permission: 'administer webform'
+
 entity.webform_image_select_images.add_form:
   path: '/admin/structure/webform/config/images/manage/add'
   defaults:
diff --git a/web/modules/webform/modules/webform_node/config/optional/field.field.node.webform.webform.yml b/web/modules/webform/modules/webform_node/config/optional/field.field.node.webform.webform.yml
index 96fd427217eabadd8f5c8b8e0c85ec2fd135b509..ab21c8fb1216cf0ef86e5d042f677385c0352089 100644
--- a/web/modules/webform/modules/webform_node/config/optional/field.field.node.webform.webform.yml
+++ b/web/modules/webform/modules/webform_node/config/optional/field.field.node.webform.webform.yml
@@ -14,13 +14,14 @@ label: Webform
 description: 'Select the webform that you would like to attach to this node.'
 required: false
 translatable: false
-default_value: {  }
+default_value:
+  - default_data: ''
+    status: open
+    open: ''
+    close: ''
+    target_uuid: ''
 default_value_callback: ''
 settings:
-  default_data: ''
-  status: open
-  open: ''
-  close: ''
   handler: 'default:webform'
   handler_settings: {  }
 field_type: webform
diff --git a/web/modules/webform/modules/webform_node/css/webform_node.entity_references.css b/web/modules/webform/modules/webform_node/css/webform_node.entity_references.css
index fa4dae9eda7a3d6cabba4aeebb770f8a737c86d5..cbc3861d363b0fed3ed2e5fc525ffa34a3e45a6b 100644
--- a/web/modules/webform/modules/webform_node/css/webform_node.entity_references.css
+++ b/web/modules/webform/modules/webform_node/css/webform_node.entity_references.css
@@ -15,7 +15,7 @@
 }
 
 /**
- * If dropbutton is in a contextual  region we need to move it to the left to
+ * If dropbutton is in a contextual region we need to move it to the left to
  * prevent it from overlapping with the pencil icon.
  */
 .js .contextual-region .webform-node-entity-references .dropbutton-widget {
diff --git a/web/modules/webform/modules/webform_node/src/Access/WebformNodeAccess.php b/web/modules/webform/modules/webform_node/src/Access/WebformNodeAccess.php
index 405bc2429e9a0f4f6322baf57a6fdaed2b7ba977..0bc7fecfc9fced4e748d5534093dce2b049c2612 100644
--- a/web/modules/webform/modules/webform_node/src/Access/WebformNodeAccess.php
+++ b/web/modules/webform/modules/webform_node/src/Access/WebformNodeAccess.php
@@ -14,6 +14,34 @@
  */
 class WebformNodeAccess {
 
+  /**
+   * Check whether the user can access a node's webform drafts.
+   *
+   * @param string $operation
+   *   Operation being performed.
+   * @param string $entity_access
+   *   Entity access rule that needs to be checked.
+   * @param \Drupal\node\NodeInterface $node
+   *   A node.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   Run access checks for this account.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public static function checkWebformDraftsAccess($operation, $entity_access, NodeInterface $node, AccountInterface $account) {
+    $access_result = static::checkAccess($operation, $entity_access, $node, NULL, $account);
+    if ($access_result->isAllowed()) {
+      /** @var \Drupal\webform\WebformEntityReferenceManagerInterface $entity_reference_manager */
+      $entity_reference_manager = \Drupal::service('webform.entity_reference_manager');
+      $webform = $entity_reference_manager->getWebform($node);
+      return WebformEntityAccess::checkDraftsAccess($webform, $node);
+    }
+    else {
+      return $access_result;
+    }
+  }
+
   /**
    * Check whether the user can access a node's webform results.
    *
@@ -121,6 +149,12 @@ public static function checkWebformSubmissionAccess($operation, $entity_access,
 
       case 'webform_submission_resend':
         return WebformSubmissionAccess::checkResendAccess($webform_submission, $account);
+
+      case 'webform_submission_duplicate':
+        /** @var \Drupal\webform\WebformEntityReferenceManagerInterface $entity_reference_manager */
+        $entity_reference_manager = \Drupal::service('webform.entity_reference_manager');
+        $webform = $entity_reference_manager->getWebform($node);
+        return WebformEntityAccess::checkWebformSettingValue($webform, 'submission_user_duplicate', TRUE);
     }
 
     return $access_result;
@@ -154,8 +188,11 @@ protected static function checkAccess($operation, $entity_access, NodeInterface
     }
 
     // Check that the webform submission was created via the webform node.
-    if ($webform_submission && $webform_submission->getSourceEntity() != $node) {
-      return AccessResult::forbidden();
+    if ($webform_submission) {
+      $source_node = $webform_submission->getSourceEntity();
+      if (!$source_node || $source_node->id() !== $node->id()) {
+        return AccessResult::forbidden();
+      }
     }
 
     // Check the node operation.
diff --git a/web/modules/webform/modules/webform_node/src/Controller/WebformNodeReferencesListController.php b/web/modules/webform/modules/webform_node/src/Controller/WebformNodeReferencesListController.php
index 9528cd814df69b36473f98da440d2f2a0932e8c1..f0a04a17b7223366a958aad7c0ef985d2f1936f4 100644
--- a/web/modules/webform/modules/webform_node/src/Controller/WebformNodeReferencesListController.php
+++ b/web/modules/webform/modules/webform_node/src/Controller/WebformNodeReferencesListController.php
@@ -113,18 +113,18 @@ public function listing(WebformInterface $webform) {
    *   The node type storage class.
    * @param \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $field_config_storage
    *   The field config storage class.
-   * @param \Drupal\webform\WebformSubmissionStorageInterface $webform_submsision_storage
+   * @param \Drupal\webform\WebformSubmissionStorageInterface $webform_submission_storage
    *   The webform submission storage class.
    * @param \Drupal\webform\WebformEntityReferenceManagerInterface $webform_entity_reference_manager
    *   The webform entity reference manager.
    */
-  public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, DateFormatterInterface $date_formatter, ConfigEntityStorageInterface $node_type_storage, ConfigEntityStorageInterface $field_config_storage, WebformSubmissionStorageInterface $webform_submsision_storage, WebformEntityReferenceManagerInterface $webform_entity_reference_manager) {
+  public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, DateFormatterInterface $date_formatter, ConfigEntityStorageInterface $node_type_storage, ConfigEntityStorageInterface $field_config_storage, WebformSubmissionStorageInterface $webform_submission_storage, WebformEntityReferenceManagerInterface $webform_entity_reference_manager) {
     parent::__construct($entity_type, $storage);
 
     $this->dateFormatter = $date_formatter;
     $this->nodeTypeStorage = $node_type_storage;
     $this->fieldConfigStorage = $field_config_storage;
-    $this->submissionStorage = $webform_submsision_storage;
+    $this->submissionStorage = $webform_submission_storage;
     $this->webformEntityReferenceManager = $webform_entity_reference_manager;
 
     $this->nodeTypes = [];
@@ -208,17 +208,10 @@ public function buildHeader() {
       'data' => $this->t('Webform status'),
       'class' => [RESPONSIVE_PRIORITY_LOW],
     ];
-    $header['results_total'] = [
-      'data' => $this->t('Total Results'),
+    $header['results'] = [
+      'data' => $this->t('Results'),
       'class' => [RESPONSIVE_PRIORITY_MEDIUM],
     ];
-    $header['results_operations'] = [
-      'data' => $this->t('Operations'),
-      'class' => [RESPONSIVE_PRIORITY_MEDIUM],
-    ];
-    $header['operations'] = [
-      'data' => '',
-    ];
     return $header + parent::buildHeader();
   }
 
@@ -240,13 +233,29 @@ public function buildRow(EntityInterface $entity) {
     $row['changed'] = $this->dateFormatter->format($entity->getChangedTime(), 'short');
     $row['node_status'] = $entity->isPublished() ? $this->t('Published') : $this->t('Not published');
     $row['webform_status'] = $this->getWebformStatus($entity);
-    $row['results_total'] = $this->submissionStorage->getTotal($this->webform, $entity);
-    $row['results_operations']['data'] = [
-      '#type' => 'operations',
-      '#links' => $this->getDefaultOperations($entity, 'results'),
-      '#prefix' => '<div class="webform-dropbutton">',
-      '#suffix' => '</div>',
-    ];
+
+    $result_total = $this->submissionStorage->getTotal($this->webform, $entity);
+    $results_access = $entity->access('submission_view_any');
+    $results_disabled = $this->webform->isResultsDisabled();
+    if ($results_disabled || !$results_access) {
+      $row['results'] = $result_total;
+    }
+    else {
+      $route_parameters = [
+        'node' => $entity->id(),
+      ];
+      $row['results'] = [
+        'data' => [
+          '#type' => 'link',
+          '#title' => $result_total,
+          '#attributes' => [
+            'aria-label' => $this->formatPlural($result_total, '@count result for @label', '@count results for @label', ['@label' => $entity->label()]),
+          ],
+          '#url' => Url::fromRoute('entity.node.webform.results_submissions', $route_parameters),
+        ],
+      ];
+    }
+
     $row['operations']['data'] = $this->buildOperations($entity);
     return $row + parent::buildRow($entity);
   }
@@ -297,38 +306,48 @@ protected function getWebformStatus(EntityInterface $entity) {
   /**
    * {@inheritdoc}
    */
-  public function getDefaultOperations(EntityInterface $entity, $type = 'edit') {
+  public function buildOperations(EntityInterface $entity) {
+    $build = [
+      '#type' => 'operations',
+      '#links' => $this->getOperations($entity),
+      '#prefix' => '<div class="webform-dropbutton">',
+      '#suffix' => '</div>',
+    ];
+
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOperations(EntityInterface $entity) {
     $route_parameters = [
       'node' => $entity->id(),
     ];
-    if ($type == 'results') {
-      $operations = [];
-      if ($entity->access('submission_view_any')) {
-        $operations['submissions'] = [
-          'title' => $this->t('Submissions'),
-          'url' => Url::fromRoute('entity.node.webform.results_submissions', $route_parameters),
-        ];
-        $operations['export'] = [
-          'title' => $this->t('Download'),
-          'url' => Url::fromRoute('entity.node.webform.results_export', $route_parameters),
-        ];
-      }
-      if ($entity->access('submission_delete_any')) {
-        $operations['clear'] = [
-          'title' => $this->t('Clear'),
-          'url' => Url::fromRoute('entity.node.webform.results_clear', $route_parameters),
-        ];
-      }
+    $operations = [];
+    if ($entity->access('update')) {
+      $operations['edit'] = [
+        'title' => $this->t('Edit'),
+        'url' => $this->ensureDestination($entity->toUrl('edit-form')),
+      ];
     }
-    else {
-      $operations = parent::getDefaultOperations($entity);
-      if ($entity->access('submission_update_any')) {
-        $operations['test'] = [
-          'title' => $this->t('Test'),
-          'weight' => 21,
-          'url' => Url::fromRoute('entity.node.webform.test_form', $route_parameters),
-        ];
-      }
+    if ($entity->access('view')) {
+      $operations['view'] = [
+        'title' => $this->t('View'),
+        'url' => $this->ensureDestination($entity->toUrl('canonical')),
+      ];
+    }
+    if ($entity->access('submission_view_any') && !$this->webform->isResultsDisabled()) {
+      $operations['results'] = [
+        'title' => $this->t('Results'),
+        'url' => Url::fromRoute('entity.node.webform.results_submissions', $route_parameters),
+      ];
+    }
+    if ($entity->access('delete')) {
+      $operations['delete'] = [
+        'title' => $this->t('Delete'),
+        'url' => $this->ensureDestination($entity->toUrl('delete-form')),
+      ];
     }
     return $operations;
   }
@@ -339,6 +358,8 @@ public function getDefaultOperations(EntityInterface $entity, $type = 'edit') {
   public function render() {
     $build = parent::render();
 
+    $build['table']['#sticky'] = TRUE;
+
     // Customize the empty message.
     $build['table']['#empty'] = $this->t('There are no webform node references.');
 
@@ -366,7 +387,6 @@ public function render() {
     }
 
     $build['#attached']['library'][] = 'webform_node/webform_node.references';
-
     return $build;
   }
 
diff --git a/web/modules/webform/modules/webform_node/src/Controller/WebformNodeSubmissionLogController.php b/web/modules/webform/modules/webform_node/src/Controller/WebformNodeSubmissionLogController.php
deleted file mode 100644
index d20a4b789bc449bf9c7728e564e8a5d0cd4c5847..0000000000000000000000000000000000000000
--- a/web/modules/webform/modules/webform_node/src/Controller/WebformNodeSubmissionLogController.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-namespace Drupal\webform_node\Controller;
-
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\webform\WebformInterface;
-use Drupal\webform\WebformSubmissionInterface;
-use Drupal\webform\Controller\WebformSubmissionLogController;
-
-/**
- * Returns responses for webform submission log routes.
- */
-class WebformNodeSubmissionLogController extends WebformSubmissionLogController {
-
-  /**
-   * Wrapper that allows the $node to be used as $source_entity.
-   */
-  public function overview(WebformInterface $webform = NULL, WebformSubmissionInterface $webform_submission = NULL, EntityInterface $node = NULL) {
-    return parent::overview($webform, $webform_submission, $node);
-  }
-
-}
diff --git a/web/modules/webform/modules/webform_node/src/Tests/Access/WebformNodeAccessPermissionsTest.php b/web/modules/webform/modules/webform_node/src/Tests/Access/WebformNodeAccessPermissionsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a41569fa70ed73a76f7cfbd8d13d5d71b094a7ec
--- /dev/null
+++ b/web/modules/webform/modules/webform_node/src/Tests/Access/WebformNodeAccessPermissionsTest.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace Drupal\webform_node\Tests\Access;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform_node\Tests\WebformNodeTestBase;
+
+/**
+ * Tests for webform node access permissions.
+ *
+ * @group WebformNode
+ */
+class WebformNodeAccessPermissionsTest extends WebformNodeTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'webform_node'];
+
+  /**
+   * Tests webform node access permissions.
+   *
+   * @see \Drupal\webform\Tests\Access\WebformAccessPermissionTest::testWebformSubmissionAccessPermissions
+   */
+  public function testAccessPermissions() {
+    global $base_path;
+
+    // Create webform node that references the contact webform.
+    $webform = Webform::load('contact');
+    $node = $this->createWebformNode('contact');
+    $nid = $node->id();
+
+    // Own webform submission user.
+    $submission_own_account = $this->drupalCreateUser([
+      'view own webform submission',
+      'edit own webform submission',
+      'delete own webform submission',
+      'access webform submission user',
+    ]);
+
+    // Any webform submission user.
+    $submission_any_account = $this->drupalCreateUser([
+      'view any webform submission',
+      'edit any webform submission',
+      'delete any webform submission',
+    ]);
+
+    /**************************************************************************/
+    // Own submission permissions (authenticated).
+    /**************************************************************************/
+
+    $this->drupalLogin($submission_own_account);
+
+    $edit = ['subject' => '{subject}', 'message' => '{message}'];
+    $sid_1 = $this->postNodeSubmission($node, $edit);
+
+    // Check view own previous submission message.
+    $this->drupalGet("node/{$nid}");
+    $this->assertRaw('You have already submitted this webform.');
+    $this->assertRaw("<a href=\"{$base_path}node/{$nid}/webform/submissions/{$sid_1}\">View your previous submission</a>.");
+
+    // Check 'view own submission' permission.
+    $this->drupalGet("node/{$nid}/webform/submissions/{$sid_1}");
+    $this->assertResponse(200);
+
+    // Check 'edit own submission' permission.
+    $this->drupalGet("node/{$nid}/webform/submissions/{$sid_1}/edit");
+    $this->assertResponse(200);
+
+    // Check 'delete own submission' permission.
+    $this->drupalGet("node/{$nid}/webform/submissions/{$sid_1}/delete");
+    $this->assertResponse(200);
+
+    $sid_2 = $this->postNodeSubmission($node, $edit);
+
+    // Check view own previous submissions message.
+    $this->drupalGet("node/{$nid}");
+    $this->assertRaw('You have already submitted this webform.');
+    $this->assertRaw("<a href=\"{$base_path}node/{$nid}/webform/submissions\">View your previous submissions</a>");
+
+    // Check view own previous submissions.
+    $this->drupalGet("node/{$nid}/webform/submissions");
+    $this->assertResponse(200);
+    $this->assertLinkByHref("{$base_path}node/{$nid}/webform/submissions/{$sid_1}");
+    $this->assertLinkByHref("{$base_path}node/{$nid}/webform/submissions/{$sid_2}");
+
+    // Check submission user duplicate returns access denied.
+    $this->drupalGet("node/{$nid}/webform/submissions/{$sid_2}/duplicate");
+    $this->assertResponse(403);
+
+    // Enable submission user duplicate.
+    $webform->setSetting('submission_user_duplicate', TRUE);
+    $webform->save();
+
+    // Check submission user duplicate returns access allows.
+    $this->drupalGet("node/{$nid}/webform/submissions/{$sid_2}/duplicate");
+    $this->assertResponse(200);
+
+    // Check webform results access denied.
+    $this->drupalGet("node/{$nid}/webform/results/submissions");
+    $this->assertResponse(403);
+
+    /**************************************************************************/
+    // Any submission permissions.
+    /**************************************************************************/
+
+    // Login as any user.
+    $this->drupalLogin($submission_any_account);
+
+    // Check webform results access allowed.
+    $this->drupalGet("node/{$nid}/webform/results/submissions");
+    $this->assertResponse(200);
+    $this->assertLinkByHref("{$base_path}node/{$nid}/webform/submission/{$sid_1}");
+    $this->assertLinkByHref("{$base_path}node/{$nid}/webform/submission/{$sid_2}");
+
+    // Check webform submission access allowed.
+    $this->drupalGet("node/{$nid}/webform/submission/{$sid_1}");
+    $this->assertResponse(200);
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_node/src/Tests/Access/WebformNodeAccessRulesTest.php b/web/modules/webform/modules/webform_node/src/Tests/Access/WebformNodeAccessRulesTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..d9a18794c711bf71367151c0de4186dc492325fc
--- /dev/null
+++ b/web/modules/webform/modules/webform_node/src/Tests/Access/WebformNodeAccessRulesTest.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace Drupal\webform_node\Tests\Access;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform_node\Tests\WebformNodeTestBase;
+
+/**
+ * Tests for webform node access rules.
+ *
+ * @group WebformNode
+ */
+class WebformNodeAccessRulesTest extends WebformNodeTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'webform_node'];
+
+  /**
+   * Tests webform node access rules.
+   *
+   * @see \Drupal\webform\Tests\WebformEntityAccessControlsTest::testAccessRules
+   */
+  public function testAccessRules() {
+    /** @var \Drupal\webform\WebformAccessRulesManagerInterface $access_rules_manager */
+    $access_rules_manager = \Drupal::service('webform.access_rules_manager');
+    $default_access_rules = $access_rules_manager->getDefaultAccessRules();
+
+    $webform = Webform::load('contact');
+
+    $node = $this->createWebformNode('contact');
+    $nid = $node->id();
+
+    $account = $this->drupalCreateUser(['access content']);
+    $rid = $account->getRoles(TRUE)[0];
+    $uid = $account->id();
+
+    /**************************************************************************/
+
+    // Log in normal user and get their rid.
+    $this->drupalLogin($account);
+
+    // Add one submission to the Webform node.
+    $edit = [
+      'name' => '{name}',
+      'email' => 'example@example.com',
+      'subject' => '{subject}',
+      'message' => '{message',
+    ];
+    $sid = $this->postNodeSubmission($node, $edit);
+
+    // Check create authenticated/anonymous access.
+    $webform->setAccessRules($default_access_rules)->save();
+    $this->drupalGet('/node/' . $node->id());
+    $this->assertFieldByName('name', $account->getAccountName());
+    $this->assertFieldByName('email', $account->getEmail());
+
+    $access_rules = [
+      'create' => [
+        'roles' => [],
+        'users' => [],
+      ],
+    ] + $default_access_rules;
+    $webform->setAccessRules($access_rules)->save();
+
+    // Check no access.
+    $this->drupalGet('/node/' . $node->id());
+    $this->assertNoFieldByName('name', $account->getAccountName());
+    $this->assertNoFieldByName('email', $account->getEmail());
+
+    $any_tests = [
+      'node/{node}/webform/results/submissions' => 'view_any',
+      'node/{node}/webform/results/download' => 'view_any',
+      'node/{node}/webform/results/clear' => 'purge_any',
+      'node/{node}/webform/submission/{webform_submission}' => 'view_any',
+      'node/{node}/webform/submission/{webform_submission}/text' => 'view_any',
+      'node/{node}/webform/submission/{webform_submission}/yaml' => 'view_any',
+      'node/{node}/webform/submission/{webform_submission}/edit' => 'update_any',
+      'node/{node}/webform/submission/{webform_submission}/delete' => 'delete_any',
+    ];
+
+    // Check that all the test paths are access denied for authenticated.
+    foreach ($any_tests as $path => $permission) {
+      $path = str_replace('{node}', $nid, $path);
+      $path = str_replace('{webform_submission}', $sid, $path);
+
+      $this->drupalGet($path);
+      $this->assertResponse(403, 'Webform returns access denied');
+    }
+
+    // Check access rules by role and user id.
+    foreach ($any_tests as $path => $permission) {
+      $path = str_replace('{node}', $nid, $path);
+      $path = str_replace('{webform_submission}', $sid, $path);
+
+      // Check access rule via role.
+      $access_rules = [
+        $permission => [
+          'roles' => [$rid],
+          'users' => [],
+        ],
+      ] + $default_access_rules;
+      $webform->setAccessRules($access_rules)->save();
+      $this->drupalGet($path);
+      $this->assertResponse(200, 'Webform allows access via role access rules');
+
+      // Check access rule via role.
+      $access_rules = [
+        $permission => [
+          'roles' => [],
+          'users' => [$uid],
+        ],
+      ] + $default_access_rules;
+      $webform->setAccessRules($access_rules)->save();
+      $this->drupalGet($path);
+      $this->assertResponse(200, 'Webform allows access via user access rules');
+    }
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeAccessTest.php b/web/modules/webform/modules/webform_node/src/Tests/WebformNodeAccessTest.php
deleted file mode 100644
index 2b8b07573560967481337ad30b7c316386526bf7..0000000000000000000000000000000000000000
--- a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeAccessTest.php
+++ /dev/null
@@ -1,197 +0,0 @@
-<?php
-
-namespace Drupal\webform_node\Tests;
-
-use Drupal\webform\Entity\Webform;
-
-/**
- * Tests for webform node access rules.
- *
- * @group WebformNode
- */
-class WebformNodeAccessTest extends WebformNodeTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = ['webform', 'webform_node'];
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
-  /**
-   * Tests webform node access perimissions.
-   *
-   * @see \Drupal\webform\Tests\WebformSubmissionAccessTest::testWebformSubmissionAccessPermissions
-   */
-  public function testAccessPermissions() {
-    global $base_path;
-
-    // Create webform node that references the contact webform.
-    $node = $this->createWebformNode('contact');
-    $nid = $node->id();
-
-    /**************************************************************************/
-    // Own submission permissions (authenticated).
-    /**************************************************************************/
-
-    $this->drupalLogin($this->ownWebformSubmissionUser);
-
-    $edit = ['subject' => '{subject}', 'message' => '{message}'];
-    $sid_1 = $this->postNodeSubmission($node, $edit);
-
-    // Check view own previous submission message.
-    $this->drupalGet("node/{$nid}");
-    $this->assertRaw('You have already submitted this webform.');
-    $this->assertRaw("<a href=\"{$base_path}node/{$nid}/webform/submissions/{$sid_1}\">View your previous submission</a>.");
-
-    // Check 'view own submission' permission.
-    $this->drupalGet("node/{$nid}/webform/submissions/{$sid_1}");
-    $this->assertResponse(200);
-
-    // Check 'edit own submission' permission.
-    $this->drupalGet("node/{$nid}/webform/submissions/{$sid_1}/edit");
-    $this->assertResponse(200);
-
-    // Check 'delete own submission' permission.
-    $this->drupalGet("node/{$nid}/webform/submissions/{$sid_1}/delete");
-    $this->assertResponse(200);
-
-    $sid_2 = $this->postNodeSubmission($node, $edit);
-
-    // Check view own previous submissions message.
-    $this->drupalGet("node/{$nid}");
-    $this->assertRaw('You have already submitted this webform.');
-    $this->assertRaw("<a href=\"{$base_path}node/{$nid}/webform/submissions\">View your previous submissions</a>");
-
-    // Check view own previous submissions.
-    $this->drupalGet("node/{$nid}/webform/submissions");
-    $this->assertResponse(200);
-    $this->assertLinkByHref("{$base_path}node/{$nid}/webform/submissions/{$sid_1}");
-    $this->assertLinkByHref("{$base_path}node/{$nid}/webform/submissions/{$sid_2}");
-
-    // Check webform results access denied.
-    $this->drupalGet("node/{$nid}/webform/results/submissions");
-    $this->assertResponse(403);
-
-    /**************************************************************************/
-    // Any submission permissions.
-    /**************************************************************************/
-
-    // Login as any user.
-    $this->drupalLogin($this->anyWebformSubmissionUser);
-
-    // Check webform results access allowed.
-    $this->drupalGet("node/{$nid}/webform/results/submissions");
-    $this->assertResponse(200);
-    $this->assertLinkByHref("{$base_path}node/{$nid}/webform/submission/{$sid_1}");
-    $this->assertLinkByHref("{$base_path}node/{$nid}/webform/submission/{$sid_2}");
-
-    // Check webform submission access allowed.
-    $this->drupalGet("node/{$nid}/webform/submission/{$sid_1}");
-    $this->assertResponse(200);
-  }
-
-  /**
-   * Tests webform node access rules.
-   *
-   * @see \Drupal\webform\Tests\WebformEntityAccessTest::testAccessRules
-   */
-  public function testAccessRules() {
-    $webform = Webform::load('contact');
-    $node = $this->createWebformNode('contact');
-    $nid = $node->id();
-
-    // Log in normal user and get their rid.
-    $this->drupalLogin($this->normalUser);
-    $roles = $this->normalUser->getRoles(TRUE);
-    $rid = reset($roles);
-    $uid = $this->normalUser->id();
-
-    // Add one submission to the Webform node.
-    $edit = [
-      'name' => '{name}',
-      'email' => 'example@example.com',
-      'subject' => '{subject}',
-      'message' => '{message',
-    ];
-    $sid = $this->postNodeSubmission($node, $edit);
-
-    // Check create authenticated/anonymous access.
-    $webform->setAccessRules(Webform::getDefaultAccessRules())->save();
-    $this->drupalGet('node/' . $node->id());
-    $this->assertFieldByName('name', $this->normalUser->getAccountName());
-    $this->assertFieldByName('email', $this->normalUser->getEmail());
-
-    $access_rules = [
-      'create' => [
-        'roles' => [],
-        'users' => [],
-      ],
-    ] + Webform::getDefaultAccessRules();
-    $webform->setAccessRules($access_rules)->save();
-
-    // Check no access.
-    $this->drupalGet('node/' . $node->id());
-    $this->assertNoFieldByName('name', $this->normalUser->getAccountName());
-    $this->assertNoFieldByName('email', $this->normalUser->getEmail());
-
-    $any_tests = [
-      'node/{node}/webform/results/submissions' => 'view_any',
-      'node/{node}/webform/results/download' => 'view_any',
-      'node/{node}/webform/results/clear' => 'purge_any',
-      'node/{node}/webform/submission/{webform_submission}' => 'view_any',
-      'node/{node}/webform/submission/{webform_submission}/text' => 'view_any',
-      'node/{node}/webform/submission/{webform_submission}/yaml' => 'view_any',
-      'node/{node}/webform/submission/{webform_submission}/edit' => 'update_any',
-      'node/{node}/webform/submission/{webform_submission}/delete' => 'delete_any',
-    ];
-
-    // Check that all the test paths are access denied for authenticated.
-    foreach ($any_tests as $path => $permission) {
-      $path = str_replace('{node}', $nid, $path);
-      $path = str_replace('{webform_submission}', $sid, $path);
-
-      $this->drupalGet($path);
-      $this->assertResponse(403, 'Webform returns access denied');
-    }
-
-    // Check access rules by role and user id.
-    foreach ($any_tests as $path => $permission) {
-      $path = str_replace('{node}', $nid, $path);
-      $path = str_replace('{webform_submission}', $sid, $path);
-
-      // Check access rule via role.
-      $access_rules = [
-        $permission => [
-          'roles' => [$rid],
-          'users' => [],
-        ],
-      ] + Webform::getDefaultAccessRules();
-      $webform->setAccessRules($access_rules)->save();
-      $this->drupalGet($path);
-      $this->assertResponse(200, 'Webform allows access via role access rules');
-
-      // Check access rule via role.
-      $access_rules = [
-        $permission => [
-          'roles' => [],
-          'users' => [$uid],
-        ],
-      ] + Webform::getDefaultAccessRules();
-      $webform->setAccessRules($access_rules)->save();
-      $this->drupalGet($path);
-      $this->assertResponse(200, 'Webform allows access via user access rules');
-    }
-  }
-
-}
diff --git a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeEntityReferenceTest.php b/web/modules/webform/modules/webform_node/src/Tests/WebformNodeEntityReferenceTest.php
index abb91fff2c2180d9939b8e087eb2e3dbb349c373..ee8cb5121328d2b85d8af2c59178907f3c0b6660 100644
--- a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeEntityReferenceTest.php
+++ b/web/modules/webform/modules/webform_node/src/Tests/WebformNodeEntityReferenceTest.php
@@ -34,7 +34,7 @@ public function testEntityReference() {
     $this->drupalLogin($this->rootUser);
 
     // Check that both webform A & B are being displayed.
-    $this->drupalGet('node/1');
+    $this->drupalGet('/node/1');
     $this->assertRaw('webform_test_multiple_a');
     $this->assertRaw('textfield_a');
     $this->assertRaw('webform_test_multiple_b');
@@ -42,59 +42,69 @@ public function testEntityReference() {
 
     /**************************************************************************/
 
-    // Check test form A (A is the default because alphabetically sorting).
-    $this->drupalGet('node/1/webform/test');
-    $this->assertRaw('textfield_a');
-    $this->assertNoRaw('textfield_b');
-
-    // Check user data is NULL.
-    $this->assertNull($user_data->get('webform_node', $this->rootUser->id(), 1));
-
-    /**************************************************************************/
-
-    // Select webform B.
-    $this->drupalGet('node/1/webform/test');
-    $this->clickLink('Test: Webform Node Multiple B');
-
-    // Check user data is set to webform B.
-    $this->assertEqual(['target_id' => 'webform_node_test_multiple_b'], $user_data->get('webform_node', $this->rootUser->id(), 1));
-
-    // Check test webform B.
-    $this->drupalGet('node/1/webform/test');
+    // Check test form B (B is the default because its weight is -1).
+    $this->drupalGet('/node/1/webform/test');
     $this->assertNoRaw('textfield_a');
     $this->assertRaw('textfield_b');
 
     // Check result webform B.
-    $this->drupalGet('node/1/webform/results/submissions');
+    $this->drupalGet('/node/1/webform/results/submissions');
     $this->assertNoRaw('textfield_a');
     $this->assertRaw('textfield_b');
 
     // Check export webform B.
-    $this->drupalGet('node/1/webform/results/download');
+    $this->drupalGet('/node/1/webform/results/download');
     $this->assertNoRaw('textfield_a');
     $this->assertRaw('textfield_b');
 
+    // Check user data is NULL.
+    $this->assertNull($user_data->get('webform_node', $this->rootUser->id(), 1));
+
+    /**************************************************************************/
+
+    // Select webform A.
+    $this->drupalGet('/node/1/webform/test');
+    $this->clickLink('Test: Webform Node Multiple A');
+
+    // Check user data is set to webform A.
+    $this->assertEqual(['target_id' => 'webform_node_test_multiple_a'], $user_data->get('webform_node', $this->rootUser->id(), 1));
+
+    // Check test webform A.
+    $this->drupalGet('/node/1/webform/test');
+    $this->assertRaw('textfield_a');
+    $this->assertNoRaw('textfield_b');
+
+    // Check result webform A.
+    $this->drupalGet('/node/1/webform/results/submissions');
+    $this->assertRaw('textfield_a');
+    $this->assertNoRaw('textfield_b');
+
+    // Check export webform A.
+    $this->drupalGet('/node/1/webform/results/download');
+    $this->assertRaw('textfield_a');
+    $this->assertNoRaw('textfield_b');
+
     /**************************************************************************/
 
     // Select webform A.
-    $this->drupalGet('node/1/webform/test');
+    $this->drupalGet('/node/1/webform/test');
     $this->clickLink('Test: Webform Node Multiple A');
 
     // Check user data is set to webform A.
     $this->assertEqual(['target_id' => 'webform_node_test_multiple_a'], $user_data->get('webform_node', $this->rootUser->id(), 1));
 
     // Check test webform A.
-    $this->drupalGet('node/1/webform/test');
+    $this->drupalGet('/node/1/webform/test');
     $this->assertRaw('textfield_a');
     $this->assertNoRaw('textfield_b');
 
     // Check result webform A.
-    $this->drupalGet('node/1/webform/results/submissions');
+    $this->drupalGet('/node/1/webform/results/submissions');
     $this->assertRaw('textfield_a');
     $this->assertNoRaw('textfield_b');
 
     // Check export webform A.
-    $this->drupalGet('node/1/webform/results/download');
+    $this->drupalGet('/node/1/webform/results/download');
     $this->assertRaw('textfield_a');
     $this->assertNoRaw('textfield_b');
 
diff --git a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeReferencesTest.php b/web/modules/webform/modules/webform_node/src/Tests/WebformNodeReferencesTest.php
index 24060c330a9e2572eef5975b58dc05aaedb860ce..3d186f3541e3480d2ba96ac7c67886b897afa8eb 100644
--- a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeReferencesTest.php
+++ b/web/modules/webform/modules/webform_node/src/Tests/WebformNodeReferencesTest.php
@@ -26,7 +26,7 @@ public function testReferences() {
     $this->drupalPlaceBlock('help_block');
 
     // Check references tab's empty message.
-    $this->drupalGet('admin/structure/webform/manage/contact/references');
+    $this->drupalGet('/admin/structure/webform/manage/contact/references');
     $this->assertRaw('There are no webform node references.');
 
     // Create webform node.
@@ -34,7 +34,7 @@ public function testReferences() {
     $node->webform->target_id = 'contact';
     $node->save();
 
-    $this->drupalGet('admin/structure/webform/manage/contact/references');
+    $this->drupalGet('/admin/structure/webform/manage/contact/references');
 
     // Check references tab does not include empty message.
     $this->assertNoRaw('There are no webform node references.');
@@ -46,12 +46,12 @@ public function testReferences() {
     $this->assertRaw('<li><a href="' . $base_path . 'node/add/webform?webform_id=contact" class="button button-action" data-drupal-link-query="{&quot;webform_id&quot;:&quot;contact&quot;}" data-drupal-link-system-path="node/add/webform">Add Webform</a></li>');
 
     // Check node with prepopulated webform.
-    $this->drupalGet('node/add/webform', ['query' => ['webform_id' => 'contact']]);
+    $this->drupalGet('/node/add/webform', ['query' => ['webform_id' => 'contact']]);
     $this->assertFieldByName('webform[0][target_id]', 'contact');
 
     // Check node without prepopulated webform warning.
-    $this->drupalGet('node/add/webform');
-    $this->assertRaw('Webforms must first be <a href="' . $base_path . 'admin/structure/webform">created</a> before referencing them in the below form.');
+    $this->drupalGet('/node/add/webform');
+    $this->assertRaw('Webforms must first be <a href="' . $base_path . 'admin/structure/webform">created</a> before referencing them.');
   }
 
 }
diff --git a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeResultsTest.php b/web/modules/webform/modules/webform_node/src/Tests/WebformNodeResultsTest.php
index ca66359a1f8f44ff4adbe8ccb6da6201ff9a9e07..17e234e23b80446b343accadeb4b3ebfe5444319 100644
--- a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeResultsTest.php
+++ b/web/modules/webform/modules/webform_node/src/Tests/WebformNodeResultsTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\webform_node\Tests;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Core\Url;
 use Drupal\webform\Entity\Webform;
 use Drupal\webform\WebformInterface;
@@ -34,10 +35,20 @@ public function setUp() {
    * Tests webform node results.
    */
   public function testResults() {
+    $normal_user = $this->drupalCreateUser();
+
+    $admin_user = $this->drupalCreateUser([
+      'administer webform',
+    ]);
+
+    $admin_submission_user = $this->drupalCreateUser([
+      'administer webform submission',
+    ]);
+
     /** @var \Drupal\webform\WebformSubmissionStorageInterface $submission_storage */
     $submission_storage = \Drupal::entityTypeManager()->getStorage('webform_submission');
 
-    $this->createUsers();
+    /**************************************************************************/
 
     $webform = Webform::load('contact');
 
@@ -47,8 +58,8 @@ public function testResults() {
     /* Webform entity reference */
 
     // Check access denied to webform results.
-    $this->drupalLogin($this->adminSubmissionUser);
-    $this->drupalGet('node/' . $node->id() . '/webform/results/submissions');
+    $this->drupalLogin($admin_submission_user);
+    $this->drupalGet('/node/' . $node->id() . '/webform/results/submissions');
     $this->assertResponse(403);
 
     // Set Node webform to the contact webform.
@@ -59,7 +70,7 @@ public function testResults() {
     /* Submission management */
 
     // Generate 3 node submissions and 3 webform submissions.
-    $this->drupalLogin($this->normalUser);
+    $this->drupalLogin($normal_user);
     $node_sids = [];
     $webform_sids = [];
     for ($i = 1; $i <= 3; $i++) {
@@ -84,27 +95,28 @@ public function testResults() {
     $this->assertEqual($submission_storage->getTotal($webform), 6);
 
     // Check webform node results.
-    $this->drupalLogin($this->adminSubmissionUser);
+    $this->drupalLogin($admin_submission_user);
     $node_route_parameters = ['node' => $node->id(), 'webform_submission' => $node_sids[1]];
     $node_submission_url = Url::fromRoute('entity.node.webform_submission.canonical', $node_route_parameters);
+    $node_submission_title = $node->label() . ': Submission #' . $node_sids[1];
     $webform_submission_route_parameters = ['webform' => 'contact', 'webform_submission' => $node_sids[1]];
     $webform_submission_url = Url::fromRoute('entity.webform_submission.canonical', $webform_submission_route_parameters);
 
-    $this->drupalGet('node/' . $node->id() . '/webform/results/submissions');
+    $this->drupalGet('/node/' . $node->id() . '/webform/results/submissions');
     $this->assertResponse(200);
     $this->assertRaw('<h1 class="page-title">' . $node->label() . '</h1>');
     $this->assertNoRaw('<h1 class="page-title">' . $webform->label() . '</h1>');
-    $this->assertRaw(('<a href="' . $node_submission_url->toString() . '">' . $node_sids[1] . '</a>'));
+    $this->assertRaw(('<a href="' . $node_submission_url->toString() . '" title="' . Html::escape($node_submission_title) . '" aria-label="' . Html::escape($node_submission_title) . '">' . $node_sids[1] . '</a>'));
     $this->assertNoRaw(('<a href="' . $webform_submission_url->toString() . '">' . $webform_sids[1] . '</a>'));
 
     // Check webform node title.
-    $this->drupalGet('node/' . $node->id() . '/webform/submission/' . $node_sids[1]);
+    $this->drupalGet('/node/' . $node->id() . '/webform/submission/' . $node_sids[1]);
     $this->assertRaw($node->label() . ': Submission #' . $node_sids[1]);
-    $this->drupalGet('node/' . $node->id() . '/webform/submission/' . $node_sids[2]);
+    $this->drupalGet('/node/' . $node->id() . '/webform/submission/' . $node_sids[2]);
     $this->assertRaw($node->label() . ': Submission #' . $node_sids[2]);
 
     // Check webform node navigation.
-    $this->drupalGet('node/' . $node->id() . '/webform/submission/' . $node_sids[1]);
+    $this->drupalGet('/node/' . $node->id() . '/webform/submission/' . $node_sids[1]);
     $node_route_parameters = ['node' => $node->id(), 'webform_submission' => $node_sids[2]];
     $node_submission_url = Url::fromRoute('entity.node.webform_submission.canonical', $node_route_parameters);
     $this->assertRaw('<a href="' . $node_submission_url->toString() . '" rel="next" title="Go to next page">Next submission <b>›</b></a>');
@@ -114,7 +126,7 @@ public function testResults() {
     $webform->save();
 
     // Check webform saved draft.
-    $this->drupalLogin($this->normalUser);
+    $this->drupalLogin($normal_user);
     $edit = [
       'name' => "nodeDraft",
       'email' => "nodeDraft@example.com",
@@ -122,26 +134,26 @@ public function testResults() {
       'message' => "Node draft message",
     ];
     $this->drupalPostForm('node/' . $node->id(), $edit, t('Save Draft'));
-    $this->drupalGet('node/' . $node->id());
+    $this->drupalGet('/node/' . $node->id());
     $this->assertRaw('A partially-completed form was found. Please complete the remaining portions.');
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertNoRaw('A partially-completed form was found. Please complete the remaining portions.');
 
     /* Table customization */
 
     // Check that access is denied to custom results table.
-    $this->drupalLogin($this->adminSubmissionUser);
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/submissions/custom');
+    $this->drupalLogin($admin_submission_user);
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/submissions/custom');
     $this->assertResponse(403);
 
     // Check that access is allowed to custom results table.
-    $this->drupalLogin($this->adminWebformUser);
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/submissions/custom');
+    $this->drupalLogin($admin_user);
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/submissions/custom');
     $this->assertResponse(200);
 
     // Check default node results table.
-    $this->drupalGet('node/' . $node->id() . '/webform/results/submissions');
-    $this->assertRaw('<th specifier="serial" aria-sort="descending" class="is-active">');
+    $this->drupalGet('/node/' . $node->id() . '/webform/results/submissions');
+    $this->assertRaw('<th specifier="created" class="priority-medium is-active" aria-sort="descending">');
     $this->assertRaw('sort by Created');
     $this->assertNoRaw('sort by Changed');
 
@@ -157,7 +169,7 @@ public function testResults() {
     $this->assertRaw('The customized table has been saved.');
 
     // Check that the webform node's results table is now customized.
-    $this->drupalGet('node/' . $node->id() . '/webform/results/submissions');
+    $this->drupalGet('/node/' . $node->id() . '/webform/results/submissions');
     $this->assertRaw('<th specifier="serial" aria-sort="ascending" class="is-active">');
     $this->assertNoRaw('sort by Created');
     $this->assertRaw('sort by Changed');
@@ -182,20 +194,20 @@ public function testResults() {
 
     // Check accessing results posted to any webform node.
     $this->drupalLogin($any_user);
-    $this->drupalGet('node/' . $node->id() . '/webform/results/submissions');
+    $this->drupalGet('/node/' . $node->id() . '/webform/results/submissions');
     $this->assertResponse(200);
 
     // Check accessing results posted to own webform node.
     $this->drupalLogin($own_user);
-    $this->drupalGet('node/' . $node->id() . '/webform/results/submissions');
+    $this->drupalGet('/node/' . $node->id() . '/webform/results/submissions');
     $this->assertResponse(403);
 
     $node->setOwnerId($own_user->id())->save();
-    $this->drupalGet('node/' . $node->id() . '/webform/results/submissions');
+    $this->drupalGet('/node/' . $node->id() . '/webform/results/submissions');
     $this->assertResponse(200);
 
     // Check deleting webform node results.
-    $this->drupalPostForm('node/' . $node->id() . '/webform/results/clear', [], t('Clear'));
+    $this->drupalPostForm('node/' . $node->id() . '/webform/results/clear', ['confirm' => TRUE], t('Clear'));
     $this->assertEqual($submission_storage->getTotal($webform, $node), 0);
     $this->assertEqual($submission_storage->getTotal($webform), 3);
   }
diff --git a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeTest.php b/web/modules/webform/modules/webform_node/src/Tests/WebformNodeTest.php
index eff3013d3dffa582fd68d5e89957aa34294dd796..796ad36ba8dff9ca9d65b35f58ff1dcecc59b3de 100644
--- a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeTest.php
+++ b/web/modules/webform/modules/webform_node/src/Tests/WebformNodeTest.php
@@ -34,9 +34,6 @@ class WebformNodeTest extends WebformNodeTestBase {
   public function setUp() {
     parent::setUp();
 
-    // Create users.
-    $this->createUsers();
-
     // Place webform test blocks.
     $this->placeWebformBlocks('webform_test_block_submission_limit');
   }
@@ -50,6 +47,10 @@ public function testNode() {
     /** @var \Drupal\webform\WebformEntityReferenceManagerInterface $entity_reference_manager */
     $entity_reference_manager = \Drupal::service('webform.entity_reference_manager');
 
+    $normal_user = $this->drupalCreateUser();
+
+    /**************************************************************************/
+
     // Check table names.
     $this->assertEqual($entity_reference_manager->getTableNames(), [
       "node__webform" => 'webform',
@@ -61,14 +62,14 @@ public function testNode() {
     /**************************************************************************/
 
     // Check contact webform.
-    $this->drupalGet('node/' . $node->id());
+    $this->drupalGet('/node/' . $node->id());
     $this->assertRaw('id="webform-submission-contact-node-' . $node->id() . '-add-form"');
     $this->assertNoFieldByName('name', 'John Smith');
 
     // Check contact webform with default data.
     $node->webform->default_data = "name: 'John Smith'";
     $node->save();
-    $this->drupalGet('node/' . $node->id());
+    $this->drupalGet('/node/' . $node->id());
     $this->assertFieldByName('name', 'John Smith');
 
     /**************************************************************************/
@@ -78,9 +79,9 @@ public function testNode() {
     // Check contact webform closed.
     $node->webform->status = WebformInterface::STATUS_CLOSED;
     $node->save();
-    $this->drupalGet('node/' . $node->id());
+    $this->drupalGet('/node/' . $node->id());
     $this->assertNoFieldByName('name', 'John Smith');
-    $this->assertRaw('Sorry...This form is closed to new submissions.');
+    $this->assertRaw('Sorry…This form is closed to new submissions.');
 
     /* Confirmation inline (test_confirmation_inline) */
 
@@ -104,7 +105,7 @@ public function testNode() {
     $node->webform->open = date('Y-m-d\TH:i:s', strtotime('today +1 day'));
     $node->webform->close = '';
     $node->save();
-    $this->drupalGet('node/' . $node->id());
+    $this->drupalGet('/node/' . $node->id());
     $this->assertRaw('This form has not yet been opened to submissions.');
     $this->assertNoFieldByName('name', 'John Smith');
 
@@ -114,7 +115,7 @@ public function testNode() {
     $node->webform->open = date('Y-m-d\TH:i:s', strtotime('today -1 day'));
     $node->webform->close = '';
     $node->save();
-    $this->drupalGet('node/' . $node->id());
+    $this->drupalGet('/node/' . $node->id());
     $this->assertNoRaw('This form has not yet been opened to submissions.');
     $this->assertFieldByName('name');
 
@@ -124,8 +125,8 @@ public function testNode() {
     $node->webform->open = '';
     $node->webform->close = date('Y-m-d\TH:i:s', strtotime('today -1 day'));
     $node->save();
-    $this->drupalGet('node/' . $node->id());
-    $this->assertRaw('Sorry...This form is closed to new submissions.');
+    $this->drupalGet('/node/' . $node->id());
+    $this->assertRaw('Sorry…This form is closed to new submissions.');
     $this->assertNoFieldByName('name');
 
     // Check scheduled and is open because open or close data was not set.
@@ -134,8 +135,8 @@ public function testNode() {
     $node->webform->open = '';
     $node->webform->close = '';
     $node->save();
-    $this->drupalGet('node/' . $node->id());
-    $this->assertNoRaw('Sorry...This form is closed to new submissions.');
+    $this->drupalGet('/node/' . $node->id());
+    $this->assertNoRaw('Sorry…This form is closed to new submissions.');
     $this->assertFieldByName('name');
 
     // Check that changes to global message clear the cache.
@@ -144,13 +145,13 @@ public function testNode() {
     $node->webform->open = '';
     $node->webform->close = date('Y-m-d\TH:i:s', strtotime('today -1 day'));
     $node->save();
-    $this->drupalGet('node/' . $node->id());
+    $this->drupalGet('/node/' . $node->id());
 
     \Drupal::configFactory()
       ->getEditable('webform.settings')
       ->set('settings.default_form_close_message', '{Custom closed message}')
       ->save();
-    $this->drupalGet('node/' . $node->id());
+    $this->drupalGet('/node/' . $node->id());
     $this->assertRaw('{Custom closed message}');
 
     /**************************************************************************/
@@ -176,7 +177,7 @@ public function testNode() {
     ]);
     $limit_form->save();
 
-    $this->drupalGet('node/' . $node->id());
+    $this->drupalGet('/node/' . $node->id());
 
     // Check submission limit blocks.
     $this->assertRaw('0 user + source entity submission(s)');
@@ -185,10 +186,10 @@ public function testNode() {
     $this->assertRaw('3 webform + source entity limit');
 
     // Create submission as authenticated user.
-    $this->drupalLogin($this->normalUser);
+    $this->drupalLogin($normal_user);
     $this->postNodeSubmission($node);
 
-    $this->drupalGet('node/' . $node->id());
+    $this->drupalGet('/node/' . $node->id());
 
     // Check per source entity user limit.
     $this->assertNoFieldByName('op', 'Submit');
@@ -208,9 +209,9 @@ public function testNode() {
     $this->postNodeSubmission($node);
     $this->drupalLogout();
 
-    $this->drupalLogin($this->normalUser);
+    $this->drupalLogin($normal_user);
 
-    $this->drupalGet('node/' . $node->id());
+    $this->drupalGet('/node/' . $node->id());
 
     // Check per source entity total limit.
     $this->assertNoFieldByName('op', 'Submit');
@@ -239,7 +240,7 @@ public function testNode() {
     $source_entity_options = ['query' => ['source_entity_type' => 'node', 'source_entity_id' => $node->id()]];
 
     // Check default data from source entity using query string.
-    $this->drupalGet('webform/contact', $source_entity_options);
+    $this->drupalGet('/webform/contact', $source_entity_options);
     $this->assertFieldByName('name', '{name}');
 
     // Check prepopulating source entity using query string.
@@ -282,7 +283,7 @@ public function testNode() {
     $node->save();
 
     // Check 'Register' link.
-    $this->drupalGet('node/' . $node->id());
+    $this->drupalGet('/node/' . $node->id());
     $this->assertLink('Register');
 
     // Check that link include source_entity_type and source_entity_id.
diff --git a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeTestBase.php b/web/modules/webform/modules/webform_node/src/Tests/WebformNodeTestBase.php
index 584d9efd14c5530aabeb90edbf84ced79a266459..96fa5bbd13250930159ecf707b85616ff7787bfe 100644
--- a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeTestBase.php
+++ b/web/modules/webform/modules/webform_node/src/Tests/WebformNodeTestBase.php
@@ -48,12 +48,16 @@ protected function postNodeSubmission(NodeInterface $node, array $edit = [], $su
    *
    * @param string $webform_id
    *   A webform id.
+   * @param array $settings
+   *   (optional) An associative array of settings for the node, as used in
+   *   entity_create().
    *
    * @return \Drupal\node\NodeInterface
    *   A webform node.
    */
-  protected function createWebformNode($webform_id) {
-    $node = $this->drupalCreateNode(['type' => 'webform']);
+  protected function createWebformNode($webform_id, array $settings = []) {
+    $settings += ['type' => 'webform'];
+    $node = $this->drupalCreateNode($settings);
     $node->webform->target_id = $webform_id;
     $node->webform->status = WebformInterface::STATUS_OPEN;
     $node->webform->open = '';
diff --git a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeTranslationTest.php b/web/modules/webform/modules/webform_node/src/Tests/WebformNodeTranslationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1a7baea2ec78dcf445cf911e6141cb94e5b78abc
--- /dev/null
+++ b/web/modules/webform/modules/webform_node/src/Tests/WebformNodeTranslationTest.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\webform_node\Tests;
+
+/**
+ * Tests for webform node translation.
+ *
+ * @group WebformNode
+ */
+class WebformNodeTranslationTest extends WebformNodeTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'webform_node', 'webform_node_test_translation'];
+
+  /**
+   * Tests webform node translation.
+   */
+  public function testNodeTranslation() {
+    $node = $this->createWebformNode('webform_node_test_translation', ['title' => 'English node']);
+
+    // Check computed token uses the English title.
+    $this->drupalGet('/node/' . $node->id());
+    $this->assertFieldByName('computed_token', 'English node');
+
+    // Create spanish node.
+    $node->addTranslation('es', ['title' => 'Spanish node'])->save();
+
+    // Check computed token uses the Spanish title.
+    $this->drupalGet('/es/node/' . $node->id());
+    $this->assertNoFieldByName('computed_token', 'English node');
+    $this->assertFieldByName('computed_token', 'Spanish node');
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_multiple/config/install/webform.webform.webform_node_test_multiple_a.yml b/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_multiple/config/install/webform.webform.webform_node_test_multiple_a.yml
index 79d17fe86bd7df6b66ea7ff25b8f73b43313dce9..6a80f24b1ea74467b14232832901a37d20bdcdc7 100644
--- a/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_multiple/config/install/webform.webform.webform_node_test_multiple_a.yml
+++ b/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_multiple/config/install/webform.webform.webform_node_test_multiple_a.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_node_test_multiple
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: webform_node_test_multiple_a
 title: 'Test: Webform Node Multiple A'
 description: 'Test Webform Node Multiple A'
@@ -16,6 +18,7 @@ elements: |
   textfield_a:
     '#title': textfield_a
     '#type': textfield
+  
 css: ''
 javascript: ''
 settings:
@@ -24,6 +27,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -31,6 +35,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -46,22 +51,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -72,6 +92,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -90,9 +111,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -145,4 +168,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_multiple/config/install/webform.webform.webform_node_test_multiple_b.yml b/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_multiple/config/install/webform.webform.webform_node_test_multiple_b.yml
index cbc93ed18d0998f019e16183f7e38143b3e18647..c93f3b99775d5e70e9c54ecdc01cdbf5974c67d1 100644
--- a/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_multiple/config/install/webform.webform.webform_node_test_multiple_b.yml
+++ b/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_multiple/config/install/webform.webform.webform_node_test_multiple_b.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_node_test_multiple
 open: null
 close: null
+weight: -1
 uid: null
 template: false
+archive: false
 id: webform_node_test_multiple_b
 title: 'Test: Webform Node Multiple B'
 description: 'Test Webform Node Multiple B'
@@ -16,6 +18,7 @@ elements: |
   textfield_b:
     '#title': textfield_b
     '#type': textfield
+  
 css: ''
 javascript: ''
 settings:
@@ -24,6 +27,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -31,6 +35,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -46,22 +51,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -72,6 +92,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -90,9 +111,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -145,4 +168,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_multiple/webform_node_test_multiple.info.yml b/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_multiple/webform_node_test_multiple.info.yml
index 3b59ee5b3b80e3bd1631cf526ae399e6679ef4dd..207e8b5ad857ca143b8bb365e4b838de87c24cf5 100644
--- a/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_multiple/webform_node_test_multiple.info.yml
+++ b/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_multiple/webform_node_test_multiple.info.yml
@@ -13,8 +13,8 @@ dependencies:
   - 'webform:webform'
   - 'webform:webform_node'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_translation/config/install/webform.webform.webform_node_test_translation.yml b/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_translation/config/install/webform.webform.webform_node_test_translation.yml
new file mode 100644
index 0000000000000000000000000000000000000000..41a5c678f6badeea7f7eed49e1ceb4d4a4afe5b6
--- /dev/null
+++ b/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_translation/config/install/webform.webform.webform_node_test_translation.yml
@@ -0,0 +1,176 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_node_test_translation
+open: null
+close: null
+weight: 0
+uid: 1
+template: false
+archive: false
+id: webform_node_test_translation
+title: webform_node_test_translation
+description: ''
+category: ''
+elements: |
+  computed_token:
+    '#type': webform_computed_token
+    '#title': computed_token
+    '#value': '[webform_submission:node:title]'
+  
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_translation/webform_node_test_translation.info.yml b/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_translation/webform_node_test_translation.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..276fba229d5c97fa9b8f8db6948f80c937e59565
--- /dev/null
+++ b/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_translation/webform_node_test_translation.info.yml
@@ -0,0 +1,16 @@
+name: 'Webform node module translation test'
+type: module
+description: 'Support module for webform node that enabled translations for webform content type.'
+package: Testing
+# core: 8.x
+dependencies:
+  - 'webform:webform'
+  - 'webform:webform_node'
+  - 'webform:webform_test_translation'
+  - 'drupal:content_translation'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_translation/webform_node_test_translation.install b/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_translation/webform_node_test_translation.install
new file mode 100644
index 0000000000000000000000000000000000000000..6fd10d5a20bb719b90d2e3dbe401dd83e0fb1ab9
--- /dev/null
+++ b/web/modules/webform/modules/webform_node/tests/modules/webform_node_test_translation/webform_node_test_translation.install
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the webform node test translation module.
+ */
+
+use Drupal\Core\Form\FormState;
+use Drupal\language\Entity\ContentLanguageSettings;
+
+/**
+ * Implements hook_install().
+ */
+function webform_node_test_translation_install() {
+  // Initialize webform node content type translation.
+  // @see \Drupal\language\Form\ContentLanguageSettingsForm::submitForm
+  $config = ContentLanguageSettings::loadByEntityTypeBundle('node', 'webform');
+  $config->setDefaultLangcode('en')
+    ->setLanguageAlterable(TRUE)
+    ->save();
+
+  // Initialize webform node content type title field translation.
+  //
+  // Below $values are from…
+  //   $values = $form_state->getValues();
+  //   var_export($values['settings']['node']['webform']); exit;
+  //
+  // within content_translation_form_language_content_settings_submit().
+  //
+  // @see /admin/config/regional/content-language
+  // @see content_translation_form_language_content_settings_submit()
+  $values = [
+    'entity_types' => ['node' => 'node'],
+    'settings' => [
+      'node' => [
+        'webform' => [
+          'settings' => [
+            'language' => [
+              'langcode' => 'site_default',
+              'language_alterable' => '1',
+            ],
+            'content_translation' => [
+              'untranslatable_fields_hide' => '1',
+            ],
+          ],
+          'fields' => [
+            'title' => 1,
+          ],
+          'translatable' => 1,
+        ],
+      ],
+    ],
+  ];
+
+  module_load_include('inc', 'content_translation', 'content_translation.admin');
+  $form_state = new FormState();
+  $form_state->setValues($values);
+  content_translation_form_language_content_settings_submit([], $form_state);
+}
diff --git a/web/modules/webform/modules/webform_node/tests/src/Kernel/WebformNodeUninstallTest.php b/web/modules/webform/modules/webform_node/tests/src/Kernel/WebformNodeUninstallTest.php
index f675bf3e35b1a9637ce64b647d4051dcf3390669..e1c056ae026823e3112f108d1b7cc928df057fc6 100644
--- a/web/modules/webform/modules/webform_node/tests/src/Kernel/WebformNodeUninstallTest.php
+++ b/web/modules/webform/modules/webform_node/tests/src/Kernel/WebformNodeUninstallTest.php
@@ -52,7 +52,7 @@ public function testWebformNodeUninstall() {
     $node = Node::create(['title' => $this->randomString(), 'type' => 'webform']);
     $node->save();
 
-    // Check webform node module can't be ininstalled.
+    // Check webform node module can't be uninstalled.
     $validation_reasons = \Drupal::service('module_installer')->validateUninstall(['webform_node']);
     $this->assertEqual(['To uninstall Webform node, delete all content that has the Webform content type.'], $validation_reasons['webform_node']);
 
diff --git a/web/modules/webform/modules/webform_node/webform_node.info.yml b/web/modules/webform/modules/webform_node/webform_node.info.yml
index 9cab5844b8aa6995b1bf88998cee4c56cae35a3d..4b6abc691bb107101865ad6ee4cdc8a4dc4262c8 100644
--- a/web/modules/webform/modules/webform_node/webform_node.info.yml
+++ b/web/modules/webform/modules/webform_node/webform_node.info.yml
@@ -11,8 +11,8 @@ dependencies:
   - 'drupal:user'
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_node/webform_node.libraries.yml b/web/modules/webform/modules/webform_node/webform_node.libraries.yml
index 75cd0709877c0a74b13586478097f25f9dcf7a25..386f0b142595532e5172b163a189095b30563c71 100644
--- a/web/modules/webform/modules/webform_node/webform_node.libraries.yml
+++ b/web/modules/webform/modules/webform_node/webform_node.libraries.yml
@@ -9,3 +9,5 @@ webform_node.references:
   css:
     theme:
       css/webform_node.references.css: {}
+  dependencies:
+    - webform/webform.admin.dropbutton
diff --git a/web/modules/webform/modules/webform_node/webform_node.links.task.yml b/web/modules/webform/modules/webform_node/webform_node.links.task.yml
index d383fa4fa3e0bc4de62e553df3713a4f54d8d1d7..b5e76f50829dbeffca5740dc811bff54a2b0788a 100644
--- a/web/modules/webform/modules/webform_node/webform_node.links.task.yml
+++ b/web/modules/webform/modules/webform_node/webform_node.links.task.yml
@@ -34,12 +34,6 @@ entity.node.webform.results_clear:
   parent_id: entity.node.webform.results
   weight: 20
 
-entity.node.webform.results_log:
-  title: 'Log'
-  route_name: entity.node.webform.results_log
-  parent_id: entity.node.webform.results
-  weight: 30
-
 # Submission.
 
 entity.node.webform_submission.canonical:
@@ -102,18 +96,6 @@ entity.node.webform_submission.resend_form:
   base_route: entity.node.webform_submission.canonical
   weight: 30
 
-entity.node.webform_submission.delete_form:
-  title: 'Delete'
-  route_name: entity.node.webform_submission.delete_form
-  base_route: entity.node.webform_submission.canonical
-  weight: 40
-
-entity.node.webform_submission.log:
-  title: 'Log'
-  route_name: entity.node.webform_submission.log
-  base_route: entity.node.webform_submission.canonical
-  weight: 50
-
 # User Submission.
 
 entity.node.webform.user.submission:
@@ -128,8 +110,8 @@ entity.node.webform.user.submission.edit:
   base_route: entity.node.webform.user.submission
   weight: 10
 
-entity.node.webform.user.submission.delete:
-  title: 'Delete'
-  route_name: entity.node.webform.user.submission.delete
+entity.node.webform.user.submission.duplicate:
+  title: 'Duplicate'
+  route_name: entity.node.webform.user.submission.duplicate
   base_route: entity.node.webform.user.submission
   weight: 20
diff --git a/web/modules/webform/modules/webform_node/webform_node.module b/web/modules/webform/modules/webform_node/webform_node.module
index 0211c34764f2b5c798b132b064872764a06632fc..25e79ba95fcb8392af9baebc624aea1a84a8b583 100644
--- a/web/modules/webform/modules/webform_node/webform_node.module
+++ b/web/modules/webform/modules/webform_node/webform_node.module
@@ -8,7 +8,6 @@
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Url;
 use Drupal\node\Entity\Node;
 use Drupal\node\NodeInterface;
@@ -26,57 +25,6 @@ function webform_node_entity_type_alter(array &$entity_types) {
   }
 }
 
-/**
- * Implements hook_help().
- */
-function webform_node_help($route_name, RouteMatchInterface $route_match) {
-  if ($route_name != 'node.add') {
-    return NULL;
-  }
-
-  // Don't show the warning is the user can't create webforms.
-  if (!\Drupal::currentUser()->hasPermission('create webform')) {
-    return NULL;
-  }
-
-  /** @var \Drupal\node\NodeTypeInterface $node_type */
-  $node_type = $route_match->getParameter('node_type');
-
-  // Determine if the node type has webform (entity reference) field.
-  $has_webform_field = FALSE;
-  $field_configs = \Drupal::entityTypeManager()->getStorage('field_config')->loadByProperties(['entity_type' => 'node', 'bundle' => $node_type->id()]);
-  foreach ($field_configs as $field_config) {
-    if ($field_config->get('field_type') === 'webform') {
-      $has_webform_field = TRUE;
-      break;
-    }
-  }
-
-  // Display warning message if webform query string parameter is missing.
-  if ($has_webform_field && !\Drupal::request()->get('webform_id')) {
-    $build = [
-      '#type' => 'webform_message',
-      '#message_type' => 'warning',
-      '#message_close' => TRUE,
-      '#message_id' => 'webform_node.references',
-      '#message_storage' => WebformMessage::STORAGE_USER,
-      '#message_message' => t('Webforms must first be <a href=":href">created</a> before referencing them in the below form.', [':href' => Url::fromRoute('entity.webform.collection')->toString()]),
-      '#cache' => ['max-age' => 0],
-    ];
-  }
-  elseif (\Drupal::request()->get('webform_id')) {
-    // If there is a webform query string parameter, then disable caching.
-    $build = ['#cache' => ['max-age' => 0]];
-  }
-  else {
-    $build = [];
-  }
-
-  // Makes sure if a webform field is added to a node type it is accounted for.
-  \Drupal::service('renderer')->addCacheableDependency($build, $node_type);
-  return $build;
-}
-
 /**
  * Implements hook_node_access().
  */
@@ -157,6 +105,49 @@ function webform_node_node_delete(NodeInterface $node) {
   $entity_reference_manager->deleteUserWebformId($node);
 }
 
+/**
+ * Implements hook_field_widget_WIDGET_TYPE_form_alter().
+ */
+function webform_node_field_widget_webform_entity_reference_autocomplete_form_alter(&$element, FormStateInterface $form_state, $context) {
+  static $once;
+  if (!empty($once)) {
+    return;
+  }
+  $once = TRUE;
+
+  // Make sure the 'target_id' is included.
+  if (!isset($element['target_id'])) {
+    return;
+  }
+
+  // Display a warning message if webform query string parameter is missing.
+  if (empty($element['target_id']['#default_value'])) {
+    $element['target_id']['#attributes']['class'][] = 'js-target-id-webform-node-references';
+    $element['webform_node_references'] = [
+      '#type' => 'webform_message',
+      '#message_type' => 'info',
+      '#message_close' => TRUE,
+      '#message_id' => 'webform_node.references',
+      '#message_storage' => WebformMessage::STORAGE_USER,
+      '#message_message' => t('Webforms must first be <a href=":href">created</a> before referencing them.', [':href' => Url::fromRoute('entity.webform.collection')->toString()]),
+      '#cache' => ['max-age' => 0],
+      '#weight' => -10,
+      '#states' => [
+        'visible' => [
+          '.js-target-id-webform-node-references' => ['value' => ''],
+        ],
+      ],
+    ];
+  }
+}
+
+/**
+ * Implements hook_field_widget_WIDGET_TYPE_form_alter().
+ */
+function webform_node_field_widget_webform_entity_reference_select_form_alter(&$element, FormStateInterface $form_state, $context) {
+  webform_node_field_widget_webform_entity_reference_autocomplete_form_alter($element, $form_state, $context);
+}
+
 /**
  * Implements hook_preprocess_HOOK() for page title templates.
  */
diff --git a/web/modules/webform/modules/webform_node/webform_node.routing.yml b/web/modules/webform/modules/webform_node/webform_node.routing.yml
index 71581efe421d066f2c8ef529df055a9a440558a9..eb135eb4f7143cc4d6e32efedce31bfa9bec2a4e 100644
--- a/web/modules/webform/modules/webform_node/webform_node.routing.yml
+++ b/web/modules/webform/modules/webform_node/webform_node.routing.yml
@@ -21,10 +21,11 @@ entity.node.webform.confirmation:
     _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformAccess'
 
 entity.node.webform.user.submissions:
-  path: '/node/{node}/webform/submissions'
+  path: '/node/{node}/webform/submissions/{submission_view}'
   defaults:
     _entity_list: 'webform_submission'
     _title: 'Submissions'
+    submission_view: ''
     operation: ''
     entity_access: 'webform.submission_view_own'
   options:
@@ -35,10 +36,11 @@ entity.node.webform.user.submissions:
     _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformAccess'
 
 entity.node.webform.user.drafts:
-  path: '/node/{node}/webform/drafts'
+  path: '/node/{node}/webform/drafts/{submission_view}'
   defaults:
     _entity_list: 'webform_submission'
     _title: 'Drafts'
+    submission_view: ''
     operation: view
     entity_access: 'webform.submission_create'
   options:
@@ -46,7 +48,7 @@ entity.node.webform.user.drafts:
       node:
         type: 'entity:node'
   requirements:
-    _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformAccess'
+    _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformDraftsAccess'
 
 entity.node.webform.user.submission:
   path: '/node/{node}/webform/submissions/{webform_submission}'
@@ -91,6 +93,21 @@ entity.node.webform.user.submission.delete:
   requirements:
     _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformSubmissionAccess'
 
+entity.node.webform.user.submission.duplicate:
+  path: '/node/{node}/webform/submissions/{webform_submission}/duplicate'
+  defaults:
+    _entity_form: 'webform_submission.duplicate'
+    _title_callback: '\Drupal\webform\Controller\WebformSubmissionViewController::title'
+    duplicate: TRUE
+    operation: webform_submission_duplicate
+    entity_access: 'webform_submission.update'
+  options:
+    parameters:
+      node:
+        type: 'entity:node'
+  requirements:
+    _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformSubmissionAccess'
+
 entity.node.webform.test_form:
   path: '/node/{node}/webform/test'
   defaults:
@@ -106,10 +123,11 @@ entity.node.webform.test_form:
     _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformAccess'
 
 entity.node.webform.results_submissions:
-  path: '/node/{node}/webform/results/submissions'
+  path: '/node/{node}/webform/results/submissions/{submission_view}'
   defaults:
     _entity_list: 'webform_submission'
     _title_callback: '\Drupal\Core\Entity\Controller\EntityController::title'
+    submission_view: ''
     operation: webform_submission_view
     entity_access: 'webform.submission_view_any'
   options:
@@ -175,22 +193,6 @@ entity.node.webform.results_clear:
   requirements:
     _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformResultsAccess'
 
-entity.node.webform.results_log:
-  path: '/node/{node}/webform/results/log'
-  defaults:
-    _controller: '\Drupal\webform_node\Controller\WebformNodeSubmissionLogController::overview'
-    _title_callback: '\Drupal\Core\Entity\Controller\EntityController::title'
-    operation: webform_submission_view
-    entity_access: 'webform.submission_view_any'
-  options:
-    _admin_route: TRUE
-    parameters:
-      node:
-        type: 'entity:node'
-  requirements:
-    _permission: 'access webform submission log'
-    _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformLogAccess'
-
 entity.node.webform_submission.canonical:
   path: '/node/{node}/webform/submission/{webform_submission}'
   defaults:
@@ -346,22 +348,6 @@ entity.node.webform_submission.delete_form:
   requirements:
     _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformSubmissionAccess'
 
-entity.node.webform_submission.log:
-  path: '/node/{node}/webform/submission/{webform_submission}/log'
-  defaults:
-    _controller: '\Drupal\webform\Controller\WebformSubmissionLogController::overview'
-    _title_callback: '\Drupal\webform\Controller\WebformSubmissionViewController::title'
-    operation: webform_submission_view
-    entity_access: 'webform.submission_view_any'
-  options:
-    _admin_route: TRUE
-    parameters:
-      node:
-        type: 'entity:node'
-  requirements:
-    _permission: 'access webform submission log'
-    _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformLogAccess'
-
 entity.node.webform.entity_reference.set:
   path: '/node/{node}/webform/change/{webform}'
   defaults:
diff --git a/web/modules/webform/modules/webform_node/webform_node.tokens.inc b/web/modules/webform/modules/webform_node/webform_node.tokens.inc
index 4896ee8434f9004c7117f249e927b3192390babd..be2aa115fae6fad6339947d5e02ce7d040110980 100644
--- a/web/modules/webform/modules/webform_node/webform_node.tokens.inc
+++ b/web/modules/webform/modules/webform_node/webform_node.tokens.inc
@@ -42,7 +42,7 @@ function webform_node_tokens($type, $tokens, array $data, array $options, Bubble
   if ($type == 'webform_submission' && !empty($data['webform_submission'])) {
     /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
     $webform_submission = $data['webform_submission'];
-    $source_entity = $webform_submission->getSourceEntity();
+    $source_entity = $webform_submission->getSourceEntity(TRUE);
     if (!$source_entity || (!$source_entity instanceof NodeInterface)) {
       return $replacements;
     }
diff --git a/web/modules/webform/modules/webform_scheduled_email/config/install/webform_scheduled_email.settings.yml b/web/modules/webform/modules/webform_scheduled_email/config/install/webform_scheduled_email.settings.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4ab1916e4b53d8ac7adeb642c89df38f6918f69d
--- /dev/null
+++ b/web/modules/webform/modules/webform_scheduled_email/config/install/webform_scheduled_email.settings.yml
@@ -0,0 +1 @@
+schedule_type: date
diff --git a/web/modules/webform/modules/webform_scheduled_email/config/schema/webform_scheduled_email.yml b/web/modules/webform/modules/webform_scheduled_email/config/schema/webform_scheduled_email.plugin.handler.schema.yml
similarity index 86%
rename from web/modules/webform/modules/webform_scheduled_email/config/schema/webform_scheduled_email.yml
rename to web/modules/webform/modules/webform_scheduled_email/config/schema/webform_scheduled_email.plugin.handler.schema.yml
index f5af571f0361c6bdc9a53be8e4b125e05355132d..aa481b914a47a9a4c1d574a9a64500812f5827fd 100644
--- a/web/modules/webform/modules/webform_scheduled_email/config/schema/webform_scheduled_email.yml
+++ b/web/modules/webform/modules/webform_scheduled_email/config/schema/webform_scheduled_email.plugin.handler.schema.yml
@@ -66,6 +66,9 @@ webform.handler.scheduled_email:
     exclude_empty:
       type: boolean
       label: 'Preview exclude empty elements'
+    exclude_empty_checkbox:
+      type: boolean
+      label: 'Exclude unselected checkboxes'
     html:
       type: boolean
       label: 'HTML'
@@ -75,6 +78,9 @@ webform.handler.scheduled_email:
     twig:
       type: boolean
       label: 'Twig'
+    theme_name:
+      type: string
+      label: 'Theme name'
     debug:
       type: boolean
       label: 'Enable debugging'
@@ -84,6 +90,12 @@ webform.handler.scheduled_email:
     days:
       type: integer
       label: 'After/before days'
+    ignore_past:
+      type: boolean
+      label: 'Prevent scheduling past actions'
     unschedule:
       type: boolean
       label: 'Unschedule email when draft or submission is saved'
+    test_send:
+      type: boolean
+      label: 'Immediately send email when testing this webform'
diff --git a/web/modules/webform/modules/webform_scheduled_email/config/schema/webform_scheduled_email.settings.schema.yml b/web/modules/webform/modules/webform_scheduled_email/config/schema/webform_scheduled_email.settings.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..099a053e5b8348531399affc3f905f2fc747e5a4
--- /dev/null
+++ b/web/modules/webform/modules/webform_scheduled_email/config/schema/webform_scheduled_email.settings.schema.yml
@@ -0,0 +1,7 @@
+webform_scheduled_email.settings:
+  type: config_object
+  label: 'Webform scheduled email settings'
+  mapping:
+    schedule_type:
+      type: string
+      label: 'Scheduled email type (date or date/time)'
diff --git a/web/modules/webform/modules/webform_scheduled_email/drush.services.yml b/web/modules/webform/modules/webform_scheduled_email/drush.services.yml
index 485cf8c1d66010dbbfe4e7edc65a657bbfa201a2..2f99e3c2ef3440b62664047ad47d165b84288105 100644
--- a/web/modules/webform/modules/webform_scheduled_email/drush.services.yml
+++ b/web/modules/webform/modules/webform_scheduled_email/drush.services.yml
@@ -1,6 +1,6 @@
 services:
   webform_scheduled_email.manager.commands:
     class: \Drupal\webform_scheduled_email\Commands\WebformScheduledEmailCommands
-    arguments: ['@webform.cli_service']
+    arguments: ['@webform_scheduled_email.manager']
     tags:
       - { name: drush.command }
diff --git a/web/modules/webform/modules/webform_scheduled_email/drush/webform_scheduled_email.drush.inc b/web/modules/webform/modules/webform_scheduled_email/drush/webform_scheduled_email.drush.inc
index 953a6b8ca03ca48aec8589f50501b2032e5fb3ef..d53a6a56f1d6383420f6c8d8b080f7fd754905ed 100644
--- a/web/modules/webform/modules/webform_scheduled_email/drush/webform_scheduled_email.drush.inc
+++ b/web/modules/webform/modules/webform_scheduled_email/drush/webform_scheduled_email.drush.inc
@@ -63,6 +63,8 @@ function webform_scheduled_email_help($section) {
  * callback to prevent conflicts with webform_scheduled_email_cron().
  *
  * @see webform_scheduled_email_cron()
+ * @see \Drupal\webform_scheduled_email\Commands\WebformScheduledEmailCommands::drush_webform_scheduled_email_cron_validate()
+ * @see \Drupal\webform_scheduled_email\Commands\WebformScheduledEmailCommands::drush_webform_scheduled_email_cron()
  */
 function webform_scheduled_email_cron_process($webform_id = NULL, $handler_id = NULL) {
   $schedule_limit = drush_get_option('schedule_limit') ?: 1000;
diff --git a/web/modules/webform/modules/webform_scheduled_email/src/Commands/WebformScheduledEmailCommands.php b/web/modules/webform/modules/webform_scheduled_email/src/Commands/WebformScheduledEmailCommands.php
index 91add816b183d9ce0c83e604a1be03b76e612b89..9c585a0f4edc9a40c6a66988f317eea74d80d7c2 100644
--- a/web/modules/webform/modules/webform_scheduled_email/src/Commands/WebformScheduledEmailCommands.php
+++ b/web/modules/webform/modules/webform_scheduled_email/src/Commands/WebformScheduledEmailCommands.php
@@ -6,12 +6,31 @@
 use Drush\Commands\DrushCommands;
 use Drupal\webform\Entity\Webform;
 use Drupal\webform_scheduled_email\Plugin\WebformHandler\ScheduleEmailWebformHandler;
+use Drupal\webform_scheduled_email\WebformScheduledEmailManagerInterface;
 
 /**
  * Webform scheduled email commands for Drush 9.x.
  */
 class WebformScheduledEmailCommands extends DrushCommands {
 
+  /**
+   * The webform scheduled email manager.
+   *
+   * @var \Drupal\webform_scheduled_email\WebformScheduledEmailManagerInterface
+   */
+  protected $manager;
+
+  /**
+   * Constructs a WebformScheduledEmailController object.
+   *
+   * @param \Drupal\webform_scheduled_email\WebformScheduledEmailManagerInterface $manager
+   *   The webform scheduled email manager.
+   */
+  public function __construct(WebformScheduledEmailManagerInterface $manager) {
+    parent::__construct();
+    $this->manager = $manager;
+  }
+
   /**
    * @hook validate webform:scheduled-email:cron
    */
@@ -56,12 +75,12 @@ public function drush_webform_scheduled_email_cron_validate(CommandData $command
    * @option send_limit
    *   The maximum number of emails to be sent. If set to 0 no emails will be sent. (Default 500)
    * @aliases wfsec
+   *
+   * @see webform_scheduled_email_cron_process()
    */
   public function drush_webform_scheduled_email_cron($webform_id = NULL, $handler_id = NULL, array $options = ['schedule_limit' => 1000, 'send_limit' => 500]) {
     $webform = ($webform_id) ? Webform::load($webform_id) : NULL;
-    /** @var \Drupal\webform_scheduled_email\WebformScheduledEmailManagerInterface $webform_scheduled_email_manager */
-    $webform_scheduled_email_manager = \Drupal::service('webform_scheduled_email.manager');
-    $stats = $webform_scheduled_email_manager->cron(
+    $stats = $this->manager->cron(
       $webform,
       $handler_id,
       $options['schedule_limit'],
diff --git a/web/modules/webform/modules/webform_scheduled_email/src/Controller/WebformScheduledEmailController.php b/web/modules/webform/modules/webform_scheduled_email/src/Controller/WebformScheduledEmailController.php
index 6202050bce3a5198a22215f994bb6cca77ffd27a..80b5c0d8ddb06dd6312692b7bae274439ec8eeea 100644
--- a/web/modules/webform/modules/webform_scheduled_email/src/Controller/WebformScheduledEmailController.php
+++ b/web/modules/webform/modules/webform_scheduled_email/src/Controller/WebformScheduledEmailController.php
@@ -44,7 +44,7 @@ public static function create(ContainerInterface $container) {
    * Runs cron task for webform scheduled email handler.
    *
    * @param \Drupal\webform\WebformInterface $webform
-   *   The webform containg a scheduled email handler.
+   *   The webform containing a scheduled email handler.
    * @param string|null $handler_id
    *   A webform handler id.
    *
@@ -53,7 +53,7 @@ public static function create(ContainerInterface $container) {
    */
   public function cron(WebformInterface $webform, $handler_id) {
     $stats = $this->manager->cron($webform, $handler_id);
-    drupal_set_message($this->t($stats['_message'], $stats['_context']));
+    $this->messenger()->addStatus($this->t($stats['_message'], $stats['_context']));
     return new RedirectResponse($webform->toUrl('handlers')->toString());
   }
 
diff --git a/web/modules/webform/modules/webform_scheduled_email/src/Plugin/WebformHandler/ScheduleEmailWebformHandler.php b/web/modules/webform/modules/webform_scheduled_email/src/Plugin/WebformHandler/ScheduleEmailWebformHandler.php
index 5f5a5cccf496a05b04e199f3da54246474a1f4d0..a7d625b26cfed66fa99c8f597fe19dbb1efab13d 100644
--- a/web/modules/webform/modules/webform_scheduled_email/src/Plugin/WebformHandler/ScheduleEmailWebformHandler.php
+++ b/web/modules/webform/modules/webform_scheduled_email/src/Plugin/WebformHandler/ScheduleEmailWebformHandler.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\webform_scheduled_email\Plugin\WebformHandler;
 
-use Drupal\Component\Utility\Unicode;
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
@@ -36,6 +35,8 @@ public function defaultConfiguration() {
       'send' => '[date:html_date]',
       'days' => '',
       'unschedule' => FALSE,
+      'ignore_past' => FALSE,
+      'test_send' => FALSE,
     ];
   }
 
@@ -101,6 +102,7 @@ public function getSummary() {
       $summary['#status'] = [
         '#type' => 'details',
         '#title' => $this->t('Scheduled email status (@total)', ['@total' => $stats['total']]),
+        '#help' => FALSE,
         '#description' => $build,
       ];
     }
@@ -111,6 +113,9 @@ public function getSummary() {
    * {@inheritdoc}
    */
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\webform_scheduled_email\WebformScheduledEmailManagerInterface $webform_scheduled_email_manager */
+    $webform_scheduled_email_manager = \Drupal::service('webform_scheduled_email.manager');
+
     $webform = $this->getWebform();
 
     // Get options, mail, and text elements as options (text/value).
@@ -144,7 +149,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
     // Send date/time.
     $send_options = [
       '[date:html_date]' => $this->t('Current date'),
-      WebformOtherBase::OTHER_OPTION => $this->t('Custom date...'),
+      WebformOtherBase::OTHER_OPTION => $this->t('Custom @label…', ['@label' => $webform_scheduled_email_manager->getDateTypeLabel()]),
       (string) $this->t('Webform') => [
         '[webform:open:html_date]' => $this->t('Open date'),
         '[webform:close:html_date]' => $this->t('Close date'),
@@ -159,13 +164,16 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       $send_options[(string) $this->t('Element')] = $date_element_options;
     }
 
+    $t_args = [
+      '@format' => $webform_scheduled_email_manager->getDateFormatLabel(),
+      '@type' => $webform_scheduled_email_manager->getDateTypeLabel(),
+    ];
     $form['scheduled']['send'] = [
       '#type' => 'webform_select_other',
       '#title' => $this->t('Send email on'),
       '#options' => $send_options,
-      '#other__placeholder' => $this->t('YYYY-MM-DD'),
-      '#other__description' => $this->t('Enter a valid ISO date (YYYY-MM-DD) or token which returns a valid ISO date.'),
-      '#parents' => ['settings', 'send'],
+      '#other__placeholder' => $webform_scheduled_email_manager->getDateFormatLabel(),
+      '#other__description' => $this->t('Enter a valid ISO @type (@format) or token which returns a valid ISO @type.', $t_args),
       '#default_value' => $this->configuration['send'],
     ];
 
@@ -186,11 +194,19 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#empty_option' => $this->t('- None -'),
       '#options' => $days_options,
       '#default_value' => $this->configuration['days'],
-      '#other__option_label' => $this->t('Custom number of days...'),
+      '#other__option_label' => $this->t('Custom number of days…'),
       '#other__type' => 'number',
       '#other__field_suffix' => $this->t('days'),
       '#other__placeholder' => $this->t('Enter +/- days'),
-      '#parents' => ['settings', 'days'],
+    ];
+
+    // Ignore past.
+    $form['scheduled']['ignore_past'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Do not schedule email if the action should be triggered in the past.'),
+      '#description' => $this->t('You can use this setting to prevent an action to be scheduled if it should have been triggered in the past.'),
+      '#default_value' => $this->configuration['ignore_past'],
+      '#return_value' => TRUE,
     ];
 
     // Unschedule.
@@ -200,7 +216,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#description' => $this->t('You can use this setting to unschedule a draft reminder, when submission has been completed.'),
       '#default_value' => $this->configuration['unschedule'],
       '#return_value' => TRUE,
-      '#parents' => ['settings', 'unschedule'],
     ];
 
     // Queue all submissions.
@@ -210,13 +225,11 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
         '#title' => $this->t('Schedule emails for all existing submissions'),
         '#description' => $this->t('Check schedule emails after submissions have been processed.'),
         '#return_value' => TRUE,
-        '#parents' => ['settings', 'queue'],
       ];
       $form['scheduled']['queue_message'] = [
         '#type' => 'webform_message',
         '#message_message' => $this->t('Please note all submissions will be rescheduled, including ones that have already received an email from this handler and submissions whose send date is in the past.'),
         '#message_type' => 'warning',
-        '#parents' => ['settings', 'queue_message'],
         '#states' => [
           'visible' => [
             ':input[name="settings[queue]"]' => [
@@ -236,20 +249,29 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#theme' => 'item_list',
       '#items' => [
         $this->t("Only one email can be scheduled per handler and submission."),
+        $this->t('Email will be rescheduled when a draft or submission is updated.'),
         $this->t("Multiple handlers can be used to schedule multiple emails."),
         $this->t('Deleting this handler will unschedule all scheduled emails.'),
         ['#markup' => $this->t('Scheduled emails are automatically sent starting at midnight using <a href=":href">cron</a>, which is executed at predefined interval.', [':href' => 'https://www.drupal.org/docs/7/setting-up-cron/overview'])],
       ],
     ];
 
-    $form['scheduled']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['scheduled']['token_tree_link'] = $this->buildTokenTreeElement();
 
     $form = parent::buildConfigurationForm($form, $form_state);
 
     // Change 'Send email' to 'Scheduled email'.
     $form['settings']['states']['#title'] = $this->t('Schedule email');
 
-    return $form;
+    // Development.
+    $form['development']['test_send'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Immediately send email when testing a webform.'),
+      '#return_value' => TRUE,
+      '#default_value' => $this->configuration['test_send'],
+    ];
+
+    return $this->setSettingsParents($form);
   }
 
   /**
@@ -258,6 +280,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
   public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
     parent::validateConfigurationForm($form, $form_state);
 
+    /** @var \Drupal\webform_scheduled_email\WebformScheduledEmailManagerInterface $webform_scheduled_email_manager */
+    $webform_scheduled_email_manager = \Drupal::service('webform_scheduled_email.manager');
+
     $values = $form_state->getValues();
 
     // Cast days string to int.
@@ -265,9 +290,15 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
 
     // If token skip validation.
     if (!preg_match('/^\[[^]]+\]$/', $values['send'])) {
+      $date_format = $webform_scheduled_email_manager->getDateFormat();
       // Validate custom 'send on' date.
-      if (!WebformDateHelper::isValidDateFormat($values['send'])) {
-        $form_state->setError($form['settings']['scheduled']['send'], $this->t('The %field date is required. Please enter a date in the format %format.', ['%field' => $this->t('Send on'), '%format' => 'YYYY-MM-DDDD']));
+      if (WebformDateHelper::createFromFormat($date_format, $values['send']) === FALSE) {
+        $t_args = [
+          '%field' => $this->t('Send on'),
+          '%format' => $webform_scheduled_email_manager->getDateFormatLabel(),
+          '@type' => $webform_scheduled_email_manager->getDateTypeLabel(),
+        ];
+        $form_state->setError($form['settings']['scheduled']['send'], $this->t('The %field date is required. Please enter a @type in the format %format.', $t_args));
       }
     }
 
@@ -286,6 +317,25 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function alterForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) {
+    // Display warning when test email will be sent immediately.
+    if (\Drupal::request()->isMethod('GET')
+      && $this->getWebform()->isTest()
+      && !empty($this->configuration['test_send'])) {
+      $t_args = ['%label' => $this->getLabel()];
+      $form['scheduled_email_handler_test_send__' . $this->getHandlerId()] = [
+        '#type' => 'webform_message',
+        '#message_message' => $this->t('The %label email will be sent immediately upon submission.', $t_args),
+        '#message_type' => 'warning',
+        '#message_close' => TRUE,
+        '#weight' => -100,
+      ];
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -349,11 +399,17 @@ protected function scheduleMessage(WebformSubmissionInterface $webform_submissio
     // Don't send the message if empty (aka To, CC, and BCC is empty).
     if (!$this->hasRecipient($webform_submission, $message)) {
       if ($this->configuration['debug']) {
-        drupal_set_message($this->t('%submission: Email <b>not sent</b> for %handler handler because a <em>To</em>, <em>CC</em>, or <em>BCC</em> email was not provided.', $t_args), 'warning');
+        $this->messenger()->addWarning($this->t('%submission: Email <b>not sent</b> for %handler handler because a <em>To</em>, <em>CC</em>, or <em>BCC</em> email was not provided.', $t_args));
       }
       return FALSE;
     }
 
+    // When testing send email immediately.
+    if ($this->getWebform()->isTest() && !empty($this->configuration['test_send'])) {
+      $this->sendMessage($webform_submission, $message);
+      return TRUE;
+    }
+
     // Get send date.
     $send_iso_date = $webform_scheduled_email_manager->getSendDate($webform_submission, $this->handler_id);
     $t_args['%date'] = $send_iso_date;
@@ -362,7 +418,7 @@ protected function scheduleMessage(WebformSubmissionInterface $webform_submissio
     // date.
     if (!$send_iso_date) {
       if ($this->configuration['debug']) {
-        drupal_set_message($this->t('%submission: Email <b>not scheduled</b> for %handler handler because %send is not a valid date/token.', $t_args), 'warning', TRUE);
+        $this->messenger()->addWarning($this->t('%submission: Email <b>not scheduled</b> for %handler handler because %send is not a valid date/token.', $t_args), TRUE);
       }
       $context = $t_args + [
         'link' => $this->getWebform()->toLink($this->t('Edit'), 'handlers')->toString(),
@@ -381,10 +437,12 @@ protected function scheduleMessage(WebformSubmissionInterface $webform_submissio
         WebformScheduledEmailManagerInterface::EMAIL_ALREADY_SCHEDULED => $this->t('Already Scheduled'),
         WebformScheduledEmailManagerInterface::EMAIL_SCHEDULED => $this->t('Scheduled'),
         WebformScheduledEmailManagerInterface::EMAIL_RESCHEDULED => $this->t('Rescheduled'),
+        WebformScheduledEmailManagerInterface::EMAIL_UNSCHEDULED => $this->t('Unscheduled'),
+        WebformScheduledEmailManagerInterface::EMAIL_IGNORED => $this->t('Ignored'),
       ];
 
-      $t_args['@action'] = Unicode::strtolower($statuses[$status]);
-      drupal_set_message($this->t('%submission: Email <b>@action</b> by %handler handler to be sent on %date.', $t_args), 'warning', TRUE);
+      $t_args['@action'] = mb_strtolower($statuses[$status]);
+      $this->messenger()->addWarning($this->t('%submission: Email <b>@action</b> by %handler handler to be sent on %date.', $t_args), TRUE);
 
       $debug_message = $this->buildDebugMessage($webform_submission, $message);
       $debug_message['status'] = [
@@ -401,7 +459,7 @@ protected function scheduleMessage(WebformSubmissionInterface $webform_submissio
         '#wrapper_attributes' => ['class' => ['container-inline'], 'style' => 'margin: 0'],
         '#weight' => -10,
       ];
-      drupal_set_message(\Drupal::service('renderer')->renderPlain($debug_message), 'warning', TRUE);
+      $this->messenger()->addWarning(\Drupal::service('renderer')->renderPlain($debug_message), TRUE);
     }
 
     return $status;
@@ -423,7 +481,7 @@ protected function unscheduleMessage(WebformSubmissionInterface $webform_submiss
           '%submission' => $webform_submission->label(),
           '%handler' => $this->label(),
         ];
-        drupal_set_message($this->t('%submission: Email <b>unscheduled</b> for %handler handler.', $t_args), 'warning', TRUE);
+        $this->messenger()->addWarning($this->t('%submission: Email <b>unscheduled</b> for %handler handler.', $t_args), TRUE);
       }
     }
   }
diff --git a/web/modules/webform/modules/webform_scheduled_email/src/Tests/WebformScheduledEmailTest.php b/web/modules/webform/modules/webform_scheduled_email/src/Tests/WebformScheduledEmailTest.php
index 5be4b1703841b8005145bfa164703876b7082100..b20e5cc7a0d51cd56bffa690cd841daa2784d052 100644
--- a/web/modules/webform/modules/webform_scheduled_email/src/Tests/WebformScheduledEmailTest.php
+++ b/web/modules/webform/modules/webform_scheduled_email/src/Tests/WebformScheduledEmailTest.php
@@ -29,8 +29,8 @@ public function testWebformScheduledEmail() {
     /** @var \Drupal\webform_scheduled_email\WebformScheduledEmailManagerInterface $scheduled_manager */
     $scheduled_manager = \Drupal::service('webform_scheduled_email.manager');
 
-    $yesterday = date('Y-m-d', strtotime('-1 days'));
-    $tomorrow = date('Y-m-d', strtotime('+1 days'));
+    $yesterday = date($scheduled_manager->getDateFormat(), strtotime('-1 days'));
+    $tomorrow = date($scheduled_manager->getDateFormat(), strtotime('+1 days'));
 
     /**************************************************************************/
     // Submission scheduling.
@@ -78,7 +78,7 @@ public function testWebformScheduledEmail() {
     $this->assertEqual($scheduled_manager->total(), 0);
 
     // Check schedule email for draft.
-    $draft_reminder = date('Y-m-d', strtotime('+14 days'));
+    $draft_reminder = date($scheduled_manager->getDateFormat(), strtotime('+14 days'));
     $sid = $this->postSubmission($webform_schedule, ['send' => 'draft_reminder'], 'Save Draft');
     $this->assertText("Test: Handler: Test scheduled email: Submission #$sid: Email scheduled by Draft reminder handler to be sent on $draft_reminder.");
     $this->assertEqual($scheduled_manager->total(), 1);
@@ -93,6 +93,28 @@ public function testWebformScheduledEmail() {
     $this->assertText("Test: Handler: Test scheduled email: Submission #$sid: Email not scheduled for Broken handler because [broken] is not a valid date/token.");
     $this->assertEqual($scheduled_manager->total($webform_schedule), 0);
 
+    /**************************************************************************/
+    // Submission scheduling with date/time.
+    /**************************************************************************/
+
+    // Change schedule type to 'datetime'.
+    \Drupal::configFactory()->getEditable('webform_scheduled_email.settings')
+      ->set('schedule_type', 'datetime')
+      ->save();
+
+    // Check other +14 days with time.
+    $sid = $this->postSubmission($webform_schedule, ['send' => 'other', 'date[date]' => '2001-01-01', 'date[time]' => '02:00:00'], 'Save Draft');
+    $webform_submission = WebformSubmission::load($sid);
+    $scheduled_email = $scheduled_manager->load($webform_submission, 'other');
+    $this->assertText("Test: Handler: Test scheduled email: Submission #$sid: Email scheduled by Other handler to be sent on 2001-01-15 02:00:00.");
+    $this->assertEqual($scheduled_email->send, strtotime('2001-01-15 02:00:00'));
+    $this->assertEqual($scheduled_email->state, $scheduled_manager::SUBMISSION_SEND);
+
+    // Change schedule type back to 'date'.
+    \Drupal::configFactory()->getEditable('webform_scheduled_email.settings')
+      ->set('schedule_type', 'date')
+      ->save();
+
     /**************************************************************************/
     // Check deleting handler removes scheduled emails.
     // @todo Figure out why the below exception is occurring during tests only.
@@ -154,6 +176,63 @@ public function testWebformScheduledEmail() {
     $this->assertEqual($scheduled_manager->waiting($webform_schedule), 6);
     $this->assertEqual($scheduled_manager->ready($webform_schedule), 0);
 
+    /**************************************************************************/
+    // Webform scheduling with conditions.
+    /**************************************************************************/
+
+    // Purge all submissions.
+    $this->purgeSubmissions();
+
+    // Create 3 yesterday scheduled emails.
+    $this->postSubmission($webform_schedule, ['send' => 'yesterday']);
+    $this->postSubmission($webform_schedule, ['send' => 'yesterday']);
+    $this->postSubmission($webform_schedule, ['send' => 'yesterday']);
+    $this->assertEqual($scheduled_manager->total($webform_schedule), 3);
+    $this->assertEqual($scheduled_manager->stats(), [
+      'total' => 3,
+      'waiting' => 0,
+      'queued' => 0,
+      'ready' => 3,
+    ]);
+
+    // Add condition to only send yesterday email if 'value' is filled.
+    /** @var \Drupal\webform\Plugin\WebformHandlerInterface $yesterday_handler */
+    $yesterday_handler = $webform_schedule->getHandler('yesterday');
+    $conditions = ['enabled' => [':input[name="value"]' => ['filled' => TRUE]]];
+    $yesterday_handler->setConditions($conditions);
+    // NOTE: Executing $webform_schedule->save() throws the below
+    // unexplainable error.
+    //
+    // TypeError: Argument 1 passed to
+    // Drupal\webform\WebformSubmissionConditionsValidator::validateConditions()
+    // must be of the type array, null given
+    // $webform_schedule->save() ;
+    //
+    // Check that 3 yesterday scheduled emails are skipped and removed.
+    $stats = $scheduled_manager->cron();
+    $this->assertEqual($stats['skipped'], 3);
+    $this->assertEqual($scheduled_manager->stats(), [
+      'total' => 0,
+      'waiting' => 0,
+      'queued' => 0,
+      'ready' => 0,
+    ]);
+
+    // Clear yesterday conditions.
+    $yesterday_handler->setConditions([]);
+
+    /**************************************************************************/
+    // Ignore past scheduling.
+    /**************************************************************************/
+
+    // Purge all submissions.
+    $this->purgeSubmissions();
+
+    // Check last year email can't be scheduled.
+    $sid = $this->postSubmission($webform_schedule, ['send' => 'last_year']);
+    $this->assertEqual($scheduled_manager->total($webform_schedule), 0);
+    $this->assertRaw('<em class="placeholder">Test: Handler: Test scheduled email: Submission #' . $sid . '</em>: Email <b>ignored</b> by <em class="placeholder">Last year</em> handler to be sent on <em class="placeholder">2016-01-01</em>.');
+
     /**************************************************************************/
     // Source entity scheduling.
     /**************************************************************************/
@@ -244,6 +323,25 @@ public function testWebformScheduledEmail() {
     // Run cron to trigger unscheduling.
     $scheduled_manager->cron();
     $this->assertEqual($scheduled_manager->total(), 0);
+
+    // Purge all submissions.
+    $this->purgeSubmissions();
+
+    /**************************************************************************/
+    // Testing.
+    /**************************************************************************/
+
+    $this->drupalLogin($this->rootUser);
+
+    // Check 'Other' email will be sent immediately message when testing.
+    $this->drupalGet('/webform/test_handler_scheduled_email/test');
+    $this->assertRaw('The <em class="placeholder">Other</em> email will be sent immediately upon submission.');
+
+    // Check 'Other' email is sent immediately via testing.
+    $this->drupalPostForm('webform/test_handler_scheduled_email/test', ['send' => 'other', 'date[date]' => '2101-01-01'], t('Submit'));
+    $this->assertEqual($scheduled_manager->total(), 0);
+    $this->assertRaw('Webform submission from: </em> sent to <em class="placeholder">simpletest@example.com</em> from <em class="placeholder">Drupal</em> [<em class="placeholder">simpletest@example.com</em>');
+    $this->assertRaw('Debug: Email: Other');
   }
 
   /**
diff --git a/web/modules/webform/modules/webform_scheduled_email/src/Tests/WebformScheduledEmailTranslationTest.php b/web/modules/webform/modules/webform_scheduled_email/src/Tests/WebformScheduledEmailTranslationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..54a0e1ec697664613131b79afa5677fe668d11fa
--- /dev/null
+++ b/web/modules/webform/modules/webform_scheduled_email/src/Tests/WebformScheduledEmailTranslationTest.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\webform_scheduled_email\Tests;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform_node\Tests\WebformNodeTestBase;
+
+/**
+ * Tests for webform scheduled email handler translation.
+ *
+ * @group WebformScheduledEmail
+ */
+class WebformScheduledEmailTranslationTest extends WebformNodeTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'webform_scheduled_email', 'webform_scheduled_email_test_translation'];
+
+  /**
+   * Tests webform schedule email handler translation.
+   */
+  public function testWebformScheduledEmailTranslation() {
+    $webform_schedule = Webform::load('test_handler_scheduled_translate');
+
+    /** @var \Drupal\webform_scheduled_email\WebformScheduledEmailManagerInterface $scheduled_manager */
+    $scheduled_manager = \Drupal::service('webform_scheduled_email.manager');
+
+    /**************************************************************************/
+
+    // Scheduled English email.
+    $this->drupalPostForm('webform/' . $webform_schedule->id(), [], t('Submit'));
+
+    // Send email.
+    $scheduled_manager->cron();
+
+    // Check that scheduled English email as sent in English.
+    $sent_email = $this->getLastEmail();
+    $this->assertEqual($sent_email['subject'], 'English Subject');
+    $this->assertEqual($sent_email['body'], 'English Body' . PHP_EOL);
+
+    // Scheduled Spanish email.
+    $this->drupalPostForm('es/webform/' . $webform_schedule->id(), [], t('Submit'));
+
+    // Send email.
+    $scheduled_manager->cron();
+
+    // Check that scheduled Spanish email as sent in Spanish.
+    $sent_email = $this->getLastEmail();
+    $this->assertEqual($sent_email['subject'], 'Spanish Subject');
+    $this->assertEqual($sent_email['body'], 'Spanish Body' . PHP_EOL);
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_scheduled_email/src/WebformScheduledEmailManager.php b/web/modules/webform/modules/webform_scheduled_email/src/WebformScheduledEmailManager.php
index 3a7b959151f3aa5805f2c5f8f1ec06a0017ffeb9..d77ef1ff475684eedd8ae7c5c796cd49ee90e2ae 100644
--- a/web/modules/webform/modules/webform_scheduled_email/src/WebformScheduledEmailManager.php
+++ b/web/modules/webform/modules/webform_scheduled_email/src/WebformScheduledEmailManager.php
@@ -2,16 +2,18 @@
 
 namespace Drupal\webform_scheduled_email;
 
+use Drupal\Component\Datetime\TimeInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Database\Query\Delete as QueryDelete;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformSubmissionInterface;
 use Drupal\webform\WebformTokenManagerInterface;
-use Psr\Log\LoggerInterface;
 
 /**
  * Defines the webform scheduled email manager.
@@ -22,6 +24,13 @@ class WebformScheduledEmailManager implements WebformScheduledEmailManagerInterf
 
   use StringTranslationTrait;
 
+  /**
+   * The time service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
   /**
    * The database connection.
    *
@@ -29,6 +38,20 @@ class WebformScheduledEmailManager implements WebformScheduledEmailManagerInterf
    */
   protected $database;
 
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * The logger factory.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+   */
+  protected $loggerFactory;
+
   /**
    * The configuration object factory.
    *
@@ -50,13 +73,6 @@ class WebformScheduledEmailManager implements WebformScheduledEmailManagerInterf
    */
   protected $submissionStorage;
 
-  /**
-   * Logger service.
-   *
-   * @var \Drupal\Core\Logger\LoggerChannelInterface
-   */
-  protected $logger;
-
   /**
    * The token manager.
    *
@@ -67,23 +83,29 @@ class WebformScheduledEmailManager implements WebformScheduledEmailManagerInterf
   /**
    * Constructs a WebformScheduledEmailManager object.
    *
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   The time service.
    * @param \Drupal\Core\Database\Connection $database
    *   The database connection.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The configuration object factory.
+   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
+   *   The logger factory.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
-   *   The entity manager.
-   * @param \Psr\Log\LoggerInterface $logger
-   *   A logger instance.
+   *   The entity type manager.
    * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
    *   The webform token manager.
    */
-  public function __construct(Connection $database, ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, LoggerInterface $logger, WebformTokenManagerInterface $token_manager) {
+  public function __construct(TimeInterface $time, Connection $database, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory, EntityTypeManagerInterface $entity_type_manager, WebformTokenManagerInterface $token_manager) {
+    $this->time = $time;
     $this->database = $database;
+    $this->languageManager = $language_manager;
     $this->configFactory = $config_factory;
+    $this->loggerFactory = $logger_factory;
     $this->webformStorage = $entity_type_manager->getStorage('webform');
     $this->submissionStorage = $entity_type_manager->getStorage('webform_submission');
-    $this->logger = $logger;
     $this->tokenManager = $token_manager;
   }
 
@@ -91,6 +113,34 @@ public function __construct(Connection $database, ConfigFactoryInterface $config
   // Scheduled message functions.
   /****************************************************************************/
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getDateType() {
+    return $this->configFactory->get('webform_scheduled_email.settings')->get('schedule_type');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDateTypeLabel() {
+    return ($this->getDateType() === 'datetime') ? $this->t('date/time') : $this->t('date');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDateFormat() {
+    return ($this->getDateType() === 'datetime') ? 'Y-m-d H:i:s' : 'Y-m-d';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDateFormatLabel() {
+    return ($this->getDateType() === 'datetime') ? 'YYYY-MM-DD HH:MM:SS' : 'YYYY-MM-DD';
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -133,6 +183,8 @@ public function getSendDate(WebformSubmissionInterface $webform_submission, $han
     // WORKAROUND:
     // Convert [*:html_date] to [*:custom:Y-m-d].
     $send = preg_replace('/^\[(date|webform_submission:(?:[^:]+)):html_date\]$/', '[\1:custom:Y-m-d]', $send);
+    // Convert [*:html_datetime] to [*:custom:Y-m-d H:i:s].
+    $send = preg_replace('/^\[(date|webform_submission:(?:[^:]+)):html_datetime\]$/', '[\1:custom:Y-m-d H:i:s]', $send);
 
     // Replace tokens.
     $send = $this->tokenManager->replace($send, $webform_submission);
@@ -147,7 +199,8 @@ public function getSendDate(WebformSubmissionInterface $webform_submission, $han
     if ($days) {
       date_add($date, date_interval_create_from_date_string("$days days"));
     }
-    return date_format($date, 'Y-m-d');
+
+    return date_format($date, $this->getDateFormat());
   }
 
   /****************************************************************************/
@@ -180,6 +233,12 @@ public function schedule(EntityInterface $entity, $handler_id) {
         return self::EMAIL_UNSCHEDULED;
       }
 
+      // Check if action should be triggered in the past.
+      if (!empty($handler_configuration['settings']['ignore_past']) && $send_timestamp < $this->time->getRequestTime()) {
+        $this->unschedule($webform_submission, $handler_id);
+        return self::EMAIL_IGNORED;
+      }
+
       // Check recipient.
       if (!$handler->hasRecipient($webform_submission, $handler->getMessage($webform_submission))) {
         $this->unschedule($webform_submission, $handler_id);
@@ -224,29 +283,18 @@ public function schedule(EntityInterface $entity, $handler_id) {
         return $status;
       }
 
-      // Log message in Drupal's log.
+      $channel = ($webform->hasSubmissionLog()) ? 'webform_submission' : 'webform';
       $context = [
+        '@title' => $webform_submission->label(),
         '@action' => $action,
-        '%submission' => $webform_submission->label(),
-        '%handler' => $handler->label(),
-        '%date' => $send_iso_date,
+        '@handler' => $handler->label(),
+        '@date' => $send_iso_date,
         'link' => $webform_submission->toLink($this->t('View'))->toString(),
+        'webform_submission' => $webform_submission,
+        'handler_id' => $handler_id,
+        'operation' => 'email ' . $action,
       ];
-      $this->logger->notice('%submission: Email @action by %handler handler to be sent on %date.', $context);
-
-      // Log scheduled email to the submission log table.
-      if ($webform->hasSubmissionLog()) {
-        $t_args = [
-          '@action' => $action,
-          '@handler' => $handler->label(),
-          '@date' => $send_iso_date,
-        ];
-        $this->submissionStorage->log($webform_submission, [
-          'handler_id' => $handler_id,
-          'operation' => 'email ' . $action,
-          'message' => $this->t("Email @action by '@handler' handler to be sent on @date.", $t_args),
-        ]);
-      }
+      $this->getLogger($channel)->notice("@title: Email @action by '@handler' handler to be sent on @date.", $context);
 
       return $status;
     }
@@ -308,22 +356,16 @@ public function unschedule(EntityInterface $entity, $handler_id = NULL) {
       $query->execute();
 
       // Log message in submission's log.
-      if ($webform->hasSubmissionLog()) {
-        $t_args = ['@handler' => $handler->label()];
-        $this->submissionStorage->log($webform_submission, [
-          'handler_id' => $handler_id,
-          'operation' => 'email unscheduled',
-          'message' => $this->t("Email unscheduled for '@handler' handler.", $t_args),
-        ]);
-      }
-
-      // Log message in Drupal's log.
+      $channel = ($webform->hasSubmissionLog()) ? 'webform_submission' : 'webform';
       $context = [
-        '%submission' => $webform_submission->label(),
-        '%handler' => $handler->label(),
+        '@title' => $webform_submission->label(),
+        '@handler' => $handler->label(),
         'link' => $webform_submission->toLink($this->t('View'))->toString(),
+        'webform_submission' => $webform_submission,
+        'handler_id' => $handler_id,
+        'operation' => 'email unscheduled',
       ];
-      $this->logger->notice('%submission: Email unscheduled for %handler handler.', $context);
+      $this->getLogger($channel)->notice("@title: Email unscheduled for '@handler' handler.", $context);
     }
     elseif ($entity instanceof WebformInterface) {
       $webform = $entity;
@@ -433,6 +475,7 @@ public function cron(EntityInterface $entity = NULL, $handler_id = NULL, $schedu
       self::EMAIL_UNSCHEDULED => $this->t('unscheduled'),
       self::EMAIL_SENT => $this->t('sent'),
       self::EMAIL_NOT_SENT => $this->t('not sent'),
+      self::EMAIL_SKIPPED => $this->t('skipped'),
     ];
     $summary = [];
     foreach ($stats as $type => $total) {
@@ -453,7 +496,7 @@ public function cron(EntityInterface $entity = NULL, $handler_id = NULL, $schedu
         $message = "@entity: Cron task executed '@handler' handler. (@summary)";
       }
     };
-    $this->logger->notice($message, $context);
+    $this->getLogger()->notice($message, $context);
     $stats['_message'] = $message;
     $stats['_context'] = $context;
 
@@ -566,6 +609,7 @@ protected function cronSend(EntityInterface $entity = NULL, $handler_id = NULL,
     $stats = [
       self::EMAIL_SENT => 0,
       self::EMAIL_NOT_SENT => 0,
+      self::EMAIL_SKIPPED => 0,
     ];
     if (empty($limit)) {
       return $stats;
@@ -594,51 +638,84 @@ protected function cronSend(EntityInterface $entity = NULL, $handler_id = NULL,
 
     $eids = [];
     foreach ($result as $record) {
+      $sid = $record->sid;
+      $webform_id = $record->webform_id;
+      $handler_id = $record->handler_id;
+
       $eids[] = $record->eid;
 
       /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
-      $webform_submission = $this->submissionStorage->load($record->sid);
+      $webform_submission = $this->submissionStorage->load($sid);
       // This should rarely happen and the orphaned record will be deleted.
       if (!$webform_submission) {
         continue;
       }
 
       $webform = $webform_submission->getWebform();
-      $handler_id = $record->handler_id;
 
       /** @var \Drupal\webform_scheduled_email\Plugin\WebformHandler\ScheduleEmailWebformHandler $handler */
-      $handler = $webform->getHandler($handler_id);
+      $handler = $webform_submission->getWebform()->getHandler($handler_id);
       // This should rarely happen and the orphaned record will be deleted.
       if (!$handler) {
         continue;
       }
 
-      // Get and send message.
-      $message = $handler->getMessage($webform_submission);
-      $status = $handler->sendMessage($webform_submission, $message);
+      if (!$handler->checkConditions($webform_submission)) {
+        // Skip sending email.
+        $action = $this->t('skipped (conditions not met)');
+        $operation = 'scheduled email skipped';
+        $stat = self::EMAIL_SKIPPED;
+      }
+      else {
+        // Switch to submission language.
+        $original_language = $this->languageManager->getConfigOverrideLanguage();
+        $switch_languages = ($webform_submission->language()->getId() !== $original_language->getId());
+        if ($switch_languages) {
+          $this->languageManager->setConfigOverrideLanguage($webform_submission->language());
+          // Reset the webform, submission, and handler.
+          $this->webformStorage->resetCache([$webform_id]);
+          $this->submissionStorage->resetCache([$sid]);
+          // Reload the webform, submission, and handler.
+          $webform = $this->webformStorage->load($webform_id);
+          $webform_submission = $this->submissionStorage->load($sid);
+          $handler = $webform->getHandler($handler_id);
+        }
 
-      $action = ($status) ? $this->t('sent') : $this->t('not sent');
+        // Send (translated) email.
+        $message = $handler->getMessage($webform_submission);
+        $status = $handler->sendMessage($webform_submission, $message);
+
+        // Switch back to original language.
+        if ($switch_languages) {
+          $this->languageManager->setConfigOverrideLanguage($original_language);
+          // Reset the webform, submission, and handler.
+          $this->webformStorage->resetCache([$webform_id]);
+          $this->submissionStorage->resetCache([$sid]);
+          // Reload the webform, submission, and handler.
+          $webform = $this->webformStorage->load($webform_id);
+          $webform_submission = $this->submissionStorage->load($sid);
+          $handler = $webform->getHandler($handler_id);
+        }
 
-      // Log scheduled email sent to submission log table.
-      if ($webform->hasSubmissionLog()) {
-        $t_args = ['@action' => $action, '@handler' => $handler->label()];
-        $this->submissionStorage->log($webform_submission, [
-          'handler_id' => $handler_id,
-          'operation' => 'scheduled email sent',
-          'message' => $this->t('Scheduled email @action for @handler handler.', $t_args),
-        ]);
+        $action = ($status) ? $this->t('sent') : $this->t('not sent');
+        $operation = ($status) ? $this->t('scheduled email sent') : $this->t('scheduled email not sent');
+        $stat = ($status) ? self::EMAIL_SENT : self::EMAIL_NOT_SENT;
       }
 
-      // Log message in Drupal's log.
+      $channel = ($webform->hasSubmissionLog()) ? 'webform_submission' : 'webform';
       $context = [
+        '@title' => $webform_submission->label(),
         '@action' => $action,
-        '%submission' => $webform->label(),
-        '%handler' => $handler->label(),
+        '@handler' => $handler->label(),
         'link' => $webform_submission->toLink($this->t('View'))->toString(),
+        'webform_submission' => $webform_submission,
+        'handler_id' => $handler_id,
+        'operation' => $operation,
       ];
-      $this->logger->notice('%submission: Scheduled email @action for %handler handler.', $context);
+      $this->getLogger($channel)->notice('Scheduled email @action for @handler handler.', $context);
 
-      $stats[$stats ? self::EMAIL_SENT : self::EMAIL_NOT_SENT]++;
+      // Increment stat.
+      $stats[$stat]++;
     }
 
     // Delete sent emails from table.
@@ -703,6 +780,19 @@ public function total(EntityInterface $entity = NULL, $handler_id = NULL, $state
   // Helper functions.
   /****************************************************************************/
 
+  /**
+   * Get webform or webform_submission logger.
+   *
+   * @param string $channel
+   *   The logger channel. Defaults to 'webform'.
+   *
+   * @return \Drupal\Core\Logger\LoggerChannelInterface
+   *   Webform logger
+   */
+  protected function getLogger($channel = 'webform') {
+    return $this->loggerFactory->get($channel);
+  }
+
   /**
    * Inspects an entity and returns the associates webform, webform submission, and/or source entity.
    *
diff --git a/web/modules/webform/modules/webform_scheduled_email/src/WebformScheduledEmailManagerInterface.php b/web/modules/webform/modules/webform_scheduled_email/src/WebformScheduledEmailManagerInterface.php
index 5b2aa03a8c3925f3e09164993cadc69765201a45..748d687cb31dac112c029a6896fc0bc6a7180650 100644
--- a/web/modules/webform/modules/webform_scheduled_email/src/WebformScheduledEmailManagerInterface.php
+++ b/web/modules/webform/modules/webform_scheduled_email/src/WebformScheduledEmailManagerInterface.php
@@ -111,6 +111,20 @@ interface WebformScheduledEmailManagerInterface {
    */
   const EMAIL_UNSCHEDULED = 'unscheduled';
 
+  /**
+   * Denote email being ignored.
+   *
+   * @var string
+   */
+  const EMAIL_IGNORED = 'ignored';
+
+  /**
+   * Denote email being skipped.
+   *
+   * @var string
+   */
+  const EMAIL_SKIPPED = 'skipped';
+
   /**
    * Denote email being sent.
    *
@@ -129,6 +143,38 @@ interface WebformScheduledEmailManagerInterface {
   // Scheduled message functions.
   /****************************************************************************/
 
+  /**
+   * Get scheduled email date type (date or datetime).
+   *
+   * @return string
+   *   Scheduled email date type (date or datetime).
+   */
+  public function getDateType();
+
+  /**
+   * Get scheduled email date label (date or date/time).
+   *
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup|string
+   *   Scheduled email date label (date or date/time).
+   */
+  public function getDateTypeLabel();
+
+  /**
+   * Get scheduled email date format (Y-m-d or Y-m-d H:i:s).
+   *
+   * @return string
+   *   Scheduled email date format (Y-m-d or Y-m-d H:i:s).
+   */
+  public function getDateFormat();
+
+  /**
+   * Get scheduled email date format label (YYYY-DD-MM or YYYY-DD-MM HH:MM:SS).
+   *
+   * @return string
+   *   Scheduled email date format label (YYYY-DD-MM or YYYY-DD-MM HH:MM:SS).
+   */
+  public function getDateFormatLabel();
+
   /**
    * Determine if submission has scheduled email for specified handler.
    *
@@ -164,8 +210,8 @@ public function load(WebformSubmissionInterface $webform_submission, $handler_id
    *   A webform handler id.
    *
    * @return string|bool
-   *   A send date using ISO date format (YYYY-MM-DD) or FALSE if the send date
-   *   is invalid.
+   *   A send date using ISO date (YYYY-MM-DD) or datetime
+   *   format (YYYY-MM-DD HH:MM:SS) or FALSE if the send date is invalid.
    */
   public function getSendDate(WebformSubmissionInterface $webform_submission, $handler_id);
 
diff --git a/web/modules/webform/modules/webform_scheduled_email/templates/webform-handler-scheduled-email-summary.html.twig b/web/modules/webform/modules/webform_scheduled_email/templates/webform-handler-scheduled-email-summary.html.twig
index 117e0a5985103bb79eba0ee543644ef8aba94791..04f8b47e1911630b0f57a56b114fde5431212f5b 100644
--- a/web/modules/webform/modules/webform_scheduled_email/templates/webform-handler-scheduled-email-summary.html.twig
+++ b/web/modules/webform/modules/webform_scheduled_email/templates/webform-handler-scheduled-email-summary.html.twig
@@ -18,6 +18,10 @@
 
 <b>{{ 'Send on:'|t }}</b> {{ settings.send }}{% if settings.days %} {{ settings.days > 0 ? '+' : ''}}{{ settings.days }} {{ 'days'|t }}{% endif %}<br />
 <b>{{ 'Unschedule:'|t }}</b> {{ settings.unschedule ? 'Yes'|t : 'No'|t }}<br />
+<b>{{ 'Ignore past:'|t }}</b> {{ settings.ignore_past ? 'Yes'|t : 'No'|t }}<br />
+{% if settings.test_send %}
+  <em>{{ 'Email will be sent immediately when testing this webform'|t }}</em><br />
+{% endif %}
 <hr />
 {% include '@webform/webform-handler-email-summary.html.twig' %}
 {% if status %}{{ status }}{% endif %}
diff --git a/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test/config/install/webform.webform.test_handler_scheduled_email.yml b/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test/config/install/webform.webform.test_handler_scheduled_email.yml
index 567f2afd80fe00b39f3edfb5ecda97d9ee182c52..ac4c817d51f99b1adc5e392c1d4f8153c3f948cf 100644
--- a/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test/config/install/webform.webform.test_handler_scheduled_email.yml
+++ b/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test/config/install/webform.webform.test_handler_scheduled_email.yml
@@ -8,13 +8,18 @@ dependencies:
     - webform_scheduled_email
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_scheduled_email
 title: 'Test: Handler: Test scheduled email'
 description: 'Test webform schedule email handler.'
 category: null
 elements: |
+  value:
+    '#type': textfield
+    '#title': value
   send:
     '#type': radios
     '#title': Send
@@ -24,11 +29,11 @@ elements: |
       last_year: 'Last year'
       draft_reminder: 'Draft reminder (+14 days)'
       broken: Broken
-      other: Other...
+      other: Other…
   date:
     '#type': datetime
     '#title': Date
-    '#description': 'Time will be ignored.'
+    '#description': 'Time maybe be ignored depending on schedule type configuration (/admin/structure/webform/config/handlers).'
     '#default_value': Now
     '#states':
       visible:
@@ -42,6 +47,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -49,6 +55,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -64,22 +71,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: true
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -90,6 +112,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: all
   draft_multiple: false
   draft_auto_save: false
@@ -108,9 +131,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -163,6 +188,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   tomorrow:
     id: scheduled_email
@@ -182,25 +211,29 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
+      theme_name: ''
       twig: false
       debug: true
       reply_to: ''
       return_path: ''
       sender_mail: ''
       sender_name: ''
-      send: '[webform_submission:completed:html_date]'
+      send: '[webform_submission:completed:html_datetime]'
       days: 1
       unschedule: false
+      ignore_past: false
+      test_send: false
   yesterday:
     id: scheduled_email
     label: Yesterday
@@ -219,25 +252,29 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
       sender_mail: ''
       sender_name: ''
-      send: '[webform_submission:completed:html_date]'
+      send: '[webform_submission:completed:html_datetime]'
       days: -1
       unschedule: false
+      ignore_past: false
+      test_send: false
   last_year:
     id: scheduled_email
     label: 'Last year'
@@ -256,17 +293,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -275,6 +314,8 @@ handlers:
       send: '2016-01-01'
       days: 0
       unschedule: false
+      ignore_past: true
+      test_send: false
   broken:
     id: scheduled_email
     label: Broken
@@ -293,17 +334,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -312,6 +355,8 @@ handlers:
       send: '[broken]'
       days: 0
       unschedule: false
+      ignore_past: false
+      test_send: false
   other:
     id: scheduled_email
     label: Other
@@ -332,25 +377,29 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
       sender_mail: ''
       sender_name: ''
-      send: '[webform_submission:values:date:html_date]'
+      send: '[webform_submission:values:date:html_datetime]'
       days: 14
       unschedule: false
+      ignore_past: false
+      test_send: true
   draft_reminder:
     id: scheduled_email
     label: 'Draft reminder'
@@ -369,22 +418,26 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
       sender_mail: ''
       sender_name: ''
-      send: '[date:html_date]'
+      send: '[date:html_datetime]'
       days: 14
       unschedule: true
+      ignore_past: false
+      test_send: false
diff --git a/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test/webform_scheduled_email_test.info.yml b/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test/webform_scheduled_email_test.info.yml
index 830ef6828bedb334073ec26e712d7af6afcc3586..6b01bfe452877e9277b237e6d8a9622dab698e60 100644
--- a/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test/webform_scheduled_email_test.info.yml
+++ b/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test/webform_scheduled_email_test.info.yml
@@ -7,8 +7,8 @@ dependencies:
   - 'webform:webform'
   - 'webform:webform_scheduled_email'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test_translation/config/install/language.entity.es.yml b/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test_translation/config/install/language.entity.es.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2d9fc7e2596649fa8f64966cc2d777c3b4ae3809
--- /dev/null
+++ b/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test_translation/config/install/language.entity.es.yml
@@ -0,0 +1,8 @@
+langcode: en
+status: true
+dependencies: {  }
+id: es
+label: Spanish
+direction: ltr
+weight: 1
+locked: false
diff --git a/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test_translation/config/install/language/es/webform.webform.test_handler_scheduled_translate.yml b/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test_translation/config/install/language/es/webform.webform.test_handler_scheduled_translate.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c608c04e028cd7cd2d6be0fa58083e50334f6c0f
--- /dev/null
+++ b/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test_translation/config/install/language/es/webform.webform.test_handler_scheduled_translate.yml
@@ -0,0 +1,5 @@
+handlers:
+  yesterday:
+    settings:
+      subject: 'Spanish Subject'
+      body: 'Spanish Body'
diff --git a/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test_translation/config/install/webform.webform.test_handler_scheduled_translate.yml b/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test_translation/config/install/webform.webform.test_handler_scheduled_translate.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a547d2cebe5041cca9b22d191cf89088b3337fd2
--- /dev/null
+++ b/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test_translation/config/install/webform.webform.test_handler_scheduled_translate.yml
@@ -0,0 +1,215 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_scheduled_email_test_translation
+  module:
+    - webform_scheduled_email
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_handler_scheduled_translate
+title: 'Test: Handler: Test scheduled email translation'
+description: 'Test webform schedule email handler translation.'
+category: null
+elements: |
+  value:
+    '#type': textfield
+    '#title': value
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: true
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  yesterday:
+    id: scheduled_email
+    label: Yesterday
+    handler_id: yesterday
+    status: true
+    conditions: {  }
+    weight: -50
+    settings:
+      states:
+        - completed
+      to_mail: _default
+      to_options: {  }
+      cc_mail: ''
+      cc_options: {  }
+      bcc_mail: ''
+      bcc_options: {  }
+      from_mail: _default
+      from_options: {  }
+      from_name: _default
+      subject: 'English Subject'
+      body: 'English Body'
+      excluded_elements: {  }
+      ignore_access: false
+      exclude_empty: true
+      exclude_empty_checkbox: false
+      html: true
+      attachments: false
+      twig: false
+      theme_name: ''
+      debug: true
+      reply_to: ''
+      return_path: ''
+      sender_mail: ''
+      sender_name: ''
+      send: '[webform_submission:completed:html_datetime]'
+      days: -1
+      unschedule: false
+      ignore_past: false
+      test_send: false
diff --git a/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test_translation/webform_scheduled_email_test_translation.info.yml b/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test_translation/webform_scheduled_email_test_translation.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..15b231361fd7c92f06f68f7d28884bd71acc4767
--- /dev/null
+++ b/web/modules/webform/modules/webform_scheduled_email/tests/modules/webform_scheduled_email_test_translation/webform_scheduled_email_test_translation.info.yml
@@ -0,0 +1,17 @@
+name: 'Webform scheduled email module translation tests'
+type: module
+description: 'Support module for webform schedule email translation that provides test forms.'
+package: Testing
+# core: 8.x
+dependencies:
+  - 'drupal:config_translation'
+  - 'drupal:language'
+  - 'drupal:locale'
+  - 'webform:webform'
+  - 'webform:webform_scheduled_email'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.info.yml b/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.info.yml
index 87c84e3ba7f29446ef3ba923fb285d79a76de1bf..8cd301cae4e6b646d7fe6ada81e1d7a4b40d0442 100644
--- a/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.info.yml
+++ b/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.info.yml
@@ -6,8 +6,8 @@ package: Webform
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.install b/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.install
index 63841ad373ef88cb33e518b2818ae2ea71e6c59b..6df1fe8fc3f8f91267f7413a9b59cc2bbaec0436 100644
--- a/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.install
+++ b/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.install
@@ -7,6 +7,9 @@
 
 use Drupal\Core\Entity\EntityTypeInterface;
 
+// Webform install helper functions.
+include_once __DIR__ . '/../../includes/webform.install.inc';
+
 /**
  * Implements hook_schema().
  */
@@ -71,3 +74,27 @@ function webform_scheduled_email_schema() {
   ];
   return $schema;
 }
+
+/**
+ * Update schema config to add new "past actions" item.
+ */
+function webform_scheduled_email_update_8001() {
+  _webform_update_webform_handler_settings();
+}
+
+/**
+ * Issue #3013016: Set a time for the schedule email handler.
+ */
+function webform_scheduled_email_update_8002() {
+  \Drupal::configFactory()
+    ->getEditable('webform_scheduled_email.settings')
+    ->set('schedule_type', 'date')
+    ->save();
+}
+
+/**
+ * Issue #3014338: Add a setting to the Scheduled Email handler to allows emails to be sent immediately when a webform is being tested.
+ */
+function webform_scheduled_email_update_8003() {
+  _webform_update_webform_handler_settings();
+}
diff --git a/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.module b/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.module
index d6d1dad23277e84af57e19c51e0a0ab509ce2fac..470755f887233ffdded9ae961f7c9d5890d1b90d 100644
--- a/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.module
+++ b/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.module
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformSubmissionInterface;
 
@@ -64,3 +65,39 @@ function webform_scheduled_email_theme() {
     ],
   ];
 }
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function webform_scheduled_email_form_webform_admin_config_handlers_form_alter(&$form, FormStateInterface $form_state) {
+  $form['webform_scheduled_email'] = [
+    '#type' => 'details',
+    '#title' => t('Scheduled email settings'),
+    '#open' => TRUE,
+    '#tree' => TRUE,
+  ];
+  $form['webform_scheduled_email']['schedule_type'] = [
+    '#type' => 'select',
+    '#title' => t('Date type'),
+    '#description' => t('Scheduled emails are queued and sent via hourly <a href="@href">cron tasks</a>. To schedule an email for a specific time, requires site administrators to increase the cron task execution frequency.', ['@href' => 'https://www.drupal.org/docs/8/cron-automated-tasks/cron-automated-tasks-overview']),
+    '#options' => [
+      'date' => t('Date (@format)', ['@format' => 'YYYY-MM-DD']),
+      'datetime' => t('Date/time (@format)', ['@format' => 'YYYY-MM-DD HH:MM:SS']),
+    ],
+    '#required' => TRUE,
+    '#default_value' => \Drupal::config('webform_scheduled_email.settings')->get('schedule_type'),
+  ];
+
+  $form['#submit'][] = '_webform_scheduled_email_form_webform_admin_config_handlers_form_submit';
+}
+
+/**
+ * Submit callback for Scheduled email settings.
+ */
+function _webform_scheduled_email_form_webform_admin_config_handlers_form_submit(&$form, FormStateInterface $form_state) {
+  $values = $form_state->getValue('webform_scheduled_email');
+  \Drupal::configFactory()
+    ->getEditable('webform_scheduled_email.settings')
+    ->set('schedule_type', $values['schedule_type'])
+    ->save();
+}
diff --git a/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.services.yml b/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.services.yml
index e11cec734cf2df5ad59ec55efa9e6de235a86415..ddfa4d710af86944fb7a00b69e0f124eabfdcd9d 100644
--- a/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.services.yml
+++ b/web/modules/webform/modules/webform_scheduled_email/webform_scheduled_email.services.yml
@@ -1,4 +1,4 @@
 services:
   webform_scheduled_email.manager:
     class: Drupal\webform_scheduled_email\WebformScheduledEmailManager
-    arguments: ['@database', '@config.factory', '@entity_type.manager', '@logger.channel.webform', '@webform.token_manager']
+    arguments: ['@datetime.time', '@database', '@language_manager', '@config.factory', '@logger.factory', '@entity_type.manager', '@webform.token_manager']
diff --git a/web/modules/webform/modules/webform_shortcuts/config/install/webform_shortcuts.settings.yml b/web/modules/webform/modules/webform_shortcuts/config/install/webform_shortcuts.settings.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b4864d4c9afbe8f9ffdc25b19e512b6f67044ce9
--- /dev/null
+++ b/web/modules/webform/modules/webform_shortcuts/config/install/webform_shortcuts.settings.yml
@@ -0,0 +1,6 @@
+add_element: 'ctrl+e'
+add_page: 'ctrl+p'
+add_layout: 'ctrl+l'
+save_elements: 'ctrl+s'
+reset_elements: 'ctrl+r'
+toggle_weights: 'ctrl+w'
diff --git a/web/modules/webform/modules/webform_shortcuts/config/schema/webform_shortcuts.settings.schema.yml b/web/modules/webform/modules/webform_shortcuts/config/schema/webform_shortcuts.settings.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..79e19fe716863957f164508e774d8a7300808918
--- /dev/null
+++ b/web/modules/webform/modules/webform_shortcuts/config/schema/webform_shortcuts.settings.schema.yml
@@ -0,0 +1,22 @@
+webform_shortcuts.settings:
+  type: config_object
+  label: 'Webform shortcuts settings'
+  mapping:
+    add_element:
+      type: string
+      label: 'Add element'
+    add_page:
+      type: string
+      label: 'Add page'
+    add_layout:
+      type: string
+      label: 'Add layout'
+    save_elements:
+      type: string
+      label: 'Save element or elements'
+    reset_elements:
+      type: string
+      label: 'Reset elements'
+    toggle_weights:
+      type: string
+      label: 'Show/hide row weights'
diff --git a/web/modules/webform/modules/webform_shortcuts/js/webform_shortcuts.js b/web/modules/webform/modules/webform_shortcuts/js/webform_shortcuts.js
new file mode 100644
index 0000000000000000000000000000000000000000..cf261eba5d867e31c57ab878bf6932945d3dcefc
--- /dev/null
+++ b/web/modules/webform/modules/webform_shortcuts/js/webform_shortcuts.js
@@ -0,0 +1,62 @@
+/**
+ * @file
+ * JavaScript behaviors for webform builder shortcuts.
+ *
+ * @see webform_shortcuts_preprocess_block()
+ */
+
+(function ($, drupalSettings) {
+
+  'use strict';
+
+  var shortcuts = drupalSettings.webform.shortcuts;
+
+  // Add element.
+  if (shortcuts.addElement) {
+    $(document).bind('keydown', shortcuts.addElement, function () {
+      $('#webform-ui-add-element').click();
+    });
+  }
+
+  // Add page.
+  if (shortcuts.addPage) {
+    $(document).bind('keydown', shortcuts.addPage, function () {
+      $('#webform-ui-add-page').focus().click();
+    });
+  }
+
+  // Add layout.
+  if (shortcuts.addLayout) {
+    $(document).bind('keydown', shortcuts.addLayout, function () {
+      $('#webform-ui-add-layout').click();
+    });
+  }
+
+  // Save element or elements.
+  if (shortcuts.saveElements) {
+    $(document).bind('keydown', shortcuts.saveElements, function () {
+      var $dialogSubmit = $('form.webform-ui-element-form [data-drupal-selector="edit-submit"]');
+      if ($dialogSubmit.length) {
+        $dialogSubmit.click();
+      }
+      else {
+        $('form.webform-edit-form [data-drupal-selector="edit-submit"]').click();
+      }
+    });
+  }
+
+  // Reset elements.
+  if (shortcuts.resetElements) {
+    $(document).bind('keydown', shortcuts.resetElements, function () {
+      $('form.webform-edit-form [data-drupal-selector="edit-reset"]').click();
+    });
+  }
+
+  // Toggle weight.
+  if (shortcuts.toggleWeights) {
+    $(document).bind('keydown', shortcuts.toggleWeights, function () {
+      $('.tabledrag-toggle-weight').eq(0).click();
+    });
+  }
+
+})(jQuery, drupalSettings);
diff --git a/web/modules/webform/modules/webform_shortcuts/tests/src/Functional/WebformShortcutsFunctionalTest.php b/web/modules/webform/modules/webform_shortcuts/tests/src/Functional/WebformShortcutsFunctionalTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..39f11e6938ac98c57e44f0031c326500ff770574
--- /dev/null
+++ b/web/modules/webform/modules/webform_shortcuts/tests/src/Functional/WebformShortcutsFunctionalTest.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\Tests\webform_shortcuts\Functional;
+
+use Drupal\Tests\webform\Functional\WebformBrowserTestBase;
+
+/**
+ * Webform shortcuts test.
+ *
+ * @group webform_browser
+ */
+class WebformShortcutsFunctionalTest extends WebformBrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'block',
+    'webform',
+    'webform_ui',
+    'webform_shortcuts',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->placeBlocks();
+  }
+
+  /**
+   * Test shortcuts.
+   */
+  public function testShortcuts() {
+    $this->drupalLogin($this->rootUser);
+
+    // Check default shortcuts.
+    $this->drupalGet('/admin/structure/webform/manage/contact');
+    $this->assertRaw('<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;Keyboard shortcuts&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;&lt;hr /&gt;CTRL+E = Add element&lt;br /&gt;CTRL+P = Add page&lt;br /&gt;CTRL+L = Add layout&lt;br /&gt;&lt;hr /&gt;CTRL+S = Save element or elements&lt;br /&gt;CTRL+R = Reset elements&lt;br /&gt;&lt;hr /&gt;CTRL+W = Show/hide row weights&lt;br /&gt;&lt;hr /&gt;&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
+
+    // Customize the shortcuts.
+    $edit = [
+      'webform_shortcuts[add_element]' => 'crtl+z',
+      'webform_shortcuts[toggle_weights]' => '',
+    ];
+    $this->drupalPostForm('/admin/structure/webform/config/advanced', $edit, t('Save configuration'));
+
+    // Check customized shortcuts.
+    $this->drupalGet('/admin/structure/webform/manage/contact');
+    $this->assertRaw('<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;Keyboard shortcuts&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;&lt;hr /&gt;CRTL+Z = Add element&lt;br /&gt;CTRL+P = Add page&lt;br /&gt;CTRL+L = Add layout&lt;br /&gt;&lt;hr /&gt;CTRL+S = Save element or elements&lt;br /&gt;CTRL+R = Reset elements&lt;br /&gt;&lt;hr /&gt;&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_shortcuts/webform_shortcuts.info.yml b/web/modules/webform/modules/webform_shortcuts/webform_shortcuts.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..542b7cf7a82e9b9d7977b408f5a8ab1aa020ccfb
--- /dev/null
+++ b/web/modules/webform/modules/webform_shortcuts/webform_shortcuts.info.yml
@@ -0,0 +1,14 @@
+name: 'Webform Shortcuts'
+type: module
+description: 'Provides configurable keyboard shortcuts to create and save webform elements.'
+package: 'Webform'
+# core: 8.x
+dependencies:
+  - 'webform:webform'
+  - 'webform:webform_ui'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_shortcuts/webform_shortcuts.install b/web/modules/webform/modules/webform_shortcuts/webform_shortcuts.install
new file mode 100644
index 0000000000000000000000000000000000000000..5442d7b5c6825ccc31beaea36fb643a856d63b16
--- /dev/null
+++ b/web/modules/webform/modules/webform_shortcuts/webform_shortcuts.install
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Webform shortcuts module.
+ */
+
+/**
+ * Issue #3040822: Allow keyboard shortcuts to be customized or disabled.
+ */
+function webform_shortcuts_update_8001() {
+  \Drupal::configFactory()
+    ->getEditable('webform_shortcuts.settings')
+    ->setData([
+      'add_element' => 'ctrl+e',
+      'add_page' => 'ctrl+p',
+      'add_layout' => 'ctrl+l',
+      'save_elements' => 'ctrl+s',
+      'reset_elements' => 'ctrl+r',
+      'toggle_weights' => 'ctrl+w',
+    ])->save();
+}
diff --git a/web/modules/webform/modules/webform_shortcuts/webform_shortcuts.libraries.yml b/web/modules/webform/modules/webform_shortcuts/webform_shortcuts.libraries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2af2ef049cdebaa54b06bbad1fbc38db45bedaed
--- /dev/null
+++ b/web/modules/webform/modules/webform_shortcuts/webform_shortcuts.libraries.yml
@@ -0,0 +1,23 @@
+webform_shortcuts:
+  version: VERSION
+  js:
+    js/webform_shortcuts.js: {}
+  dependencies:
+    - core/jquery
+    - webform_shortcuts/libraries.jquery.hotkeys
+
+# External libraries.
+
+libraries.jquery.hotkeys:
+  remote: https://github.com/jeresig/jquery.hotkeys
+  version: '0.2.0'
+  license:
+    name: MIT
+    url: https://github.com/rvera/image-picker/blob/master/LICENSE
+    gpl-compatible: true
+  cdn:
+    /libraries/jquery.hotkeys/: https://cdn.rawgit.com/jeresig/jquery.hotkeys/0.2.0/
+  js:
+    /libraries/jquery.hotkeys/jquery.hotkeys.js: {}
+  dependencies:
+    - core/jquery
diff --git a/web/modules/webform/modules/webform_shortcuts/webform_shortcuts.module b/web/modules/webform/modules/webform_shortcuts/webform_shortcuts.module
new file mode 100644
index 0000000000000000000000000000000000000000..a41597c1aa3ecd7690a3fd8aa23424d5f0a339cb
--- /dev/null
+++ b/web/modules/webform/modules/webform_shortcuts/webform_shortcuts.module
@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * @file
+ * Provides keyboard shortcuts to create and save webform elements.
+ */
+
+use Drupal\Core\Url;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Utility\WebformTextHelper;
+
+/**
+ * Implements hook_webform_libraries_info().
+ */
+function webform_shortcuts_webform_libraries_info() {
+  $libraries = [];
+  $libraries['jquery.hotkeys'] = [
+    'title' => t('jQuery.Hotkeys'),
+    'description' => t('jQuery Hotkeys is a plug-in that lets you easily add and remove handlers for keyboard events anywhere in your code supporting almost any key combination.'),
+    'notes' => t('jQuery Hotkeys is used by the form builder to quickly add and save elements.'),
+    'homepage_url' => Url::fromUri('https://github.com/jeresig/jquery.hotkeys'),
+    'download_url' => Url::fromUri('https://github.com/jeresig/jquery.hotkeys/archive/0.2.0.zip'),
+    'version' => '0.2.0',
+    'optional' => FALSE,
+  ];
+  return $libraries;
+}
+
+/**
+ * Implements hook_preprocess_block().
+ */
+function webform_shortcuts_preprocess_block(&$variables) {
+  if ($variables['plugin_id'] != 'local_actions_block') {
+    return;
+  }
+
+  if (\Drupal::routeMatch()->getRouteName() !== 'entity.webform.edit_form') {
+    return;
+  }
+
+  // Get shortcuts settings.
+  $config = \Drupal::config('webform_shortcuts.settings');
+
+  // Shortcuts.
+  $shortcuts = [
+    '--',
+    'add_element' => t('Add element'),
+    'add_page' => t('Add page'),
+    'add_layout' => t('Add layout'),
+    '--',
+    'save_elements' => t('Save element or elements'),
+    'reset_elements' => t('Reset elements'),
+    '--',
+    'toggle_weights' => t('Show/hide row weights'),
+    '--',
+  ];
+
+  $items = [];
+  $last_shortcut = '';
+  foreach ($shortcuts as $name => $task) {
+    if ($task === '--') {
+      if ($last_shortcut !== $task) {
+        $items[] = ['#markup' => '<hr/>'];
+      }
+      $last_shortcut = $task;
+    }
+    else {
+      $shortcut = $config->get($name);
+      if ($shortcut) {
+        $t_args = [
+          '@shortcut' => strtoupper($shortcut),
+          '@task' => $task,
+        ];
+        $items[] = [
+          '#markup' => t('@shortcut = @task', $t_args),
+          '#suffix' => '<br/>',
+        ];
+        $last_shortcut = $name;
+      }
+    }
+  }
+
+  $variables['content']['help'] = [
+    '#type' => 'webform_help',
+    '#help_title' => t('Keyboard shortcuts'),
+    '#help' => $items,
+  ];
+
+  $variables['content']['help']['#attached']['library'][] = 'webform_shortcuts/webform_shortcuts';
+
+  // Convert shortcuts PHP settings to camelCase JavaScript settings.
+  $settings = [];
+  $data = $config->getRawData();
+  foreach ($data as $name => $value) {
+    $settings[WebformTextHelper::snakeToCamel($name)] = $value;
+  }
+  $variables['content']['help']['#attached']['drupalSettings']['webform']['shortcuts'] = $settings;
+
+  // Add config to render array caching.
+  $renderer = \Drupal::service('renderer');
+  $renderer->addCacheableDependency($variables['content']['help'], $config);
+
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function webform_shortcuts_form_webform_admin_config_advanced_form_alter(&$form, FormStateInterface $form_state) {
+  $config = \Drupal::config('webform_shortcuts.settings');
+
+  $form['webform_shortcuts'] = [
+    '#type' => 'details',
+    '#title' => t('Keyboard shortcut settings'),
+    '#description' => t('Enter custom keyboard shortcuts for common form builder actions. Leave blank to disable an individual shortcut.') . '<br/>' .
+      t('<a href=":href">Learn more about configuring shortcuts using the jQuery HotKeys library</a>', [':href' => 'https://github.com/jeresig/jquery.hotkeys']),
+    '#open' => TRUE,
+    '#tree' => TRUE,
+  ];
+  $form['webform_shortcuts']['add_element'] = [
+    '#type' => 'textfield',
+    '#title' => t('Add element'),
+    '#default_value' => $config->get('add_element'),
+  ];
+  $form['webform_shortcuts']['add_page'] = [
+    '#type' => 'textfield',
+    '#title' => t('Add page'),
+    '#default_value' => $config->get('add_page'),
+  ];
+  $form['webform_shortcuts']['add_layout'] = [
+    '#type' => 'textfield',
+    '#title' => t('Add layout'),
+    '#default_value' => $config->get('add_layout'),
+  ];
+  $form['webform_shortcuts']['save_elements'] = [
+    '#type' => 'textfield',
+    '#title' => t('Save element or elements'),
+    '#default_value' => $config->get('save_elements'),
+  ];
+  $form['webform_shortcuts']['reset_elements'] = [
+    '#type' => 'textfield',
+    '#title' => t('Reset elements'),
+    '#default_value' => $config->get('reset_elements'),
+  ];
+  $form['webform_shortcuts']['toggle_weights'] = [
+    '#type' => 'textfield',
+    '#title' => t('Show/hide row weights'),
+    '#default_value' => $config->get('reset_elements'),
+  ];
+
+  $form['#submit'][] = '_webform_shortcuts_form_webform_admin_config_advanced_form_submit';
+}
+
+/**
+ * Submit callback to save the webform shortcuts setting.
+ */
+function _webform_shortcuts_form_webform_admin_config_advanced_form_submit($form, FormStateInterface $form_state) {
+  \Drupal::configFactory()
+    ->getEditable('webform_shortcuts.settings')
+    ->setData($form_state->getValue('webform_shortcuts'))
+    ->save();
+}
diff --git a/web/modules/webform/modules/webform_submission_export_import/src/Controller/WebformSubmissionExportImportController.php b/web/modules/webform/modules/webform_submission_export_import/src/Controller/WebformSubmissionExportImportController.php
new file mode 100644
index 0000000000000000000000000000000000000000..39a35ad21f2499359710990cc2547f0b4849e8d1
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/src/Controller/WebformSubmissionExportImportController.php
@@ -0,0 +1,177 @@
+<?php
+
+namespace Drupal\webform_submission_export_import\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Serialization\Yaml;
+use Drupal\webform\WebformRequestInterface;
+use Drupal\webform\WebformSubmissionGenerateInterface;
+use Drupal\webform_submission_export_import\WebformSubmissionExportImportImporterInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\StreamedResponse;
+
+/**
+ * Provides route responses for webform submission export/import.
+ */
+class WebformSubmissionExportImportController extends ControllerBase implements ContainerInjectionInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The webform submission storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $webformSubmissionStorage;
+
+  /**
+   * Webform request handler.
+   *
+   * @var \Drupal\webform\WebformRequestInterface
+   */
+  protected $requestHandler;
+
+  /**
+   * The webform submission generation service.
+   *
+   * @var \Drupal\webform\WebformSubmissionGenerateInterface
+   */
+  protected $generate;
+
+  /**
+   * The webform submission exporter.
+   *
+   * @var \Drupal\webform_submission_export_import\WebformSubmissionExportImportImporterInterface
+   */
+  protected $importer;
+
+  /**
+   * Constructs a WebformSubmissionExportImportController object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\webform\WebformRequestInterface $request_handler
+   *   The webform request handler.
+   * @param \Drupal\webform\WebformSubmissionGenerateInterface $submission_generate
+   *   The webform submission generation service.
+   * @param \Drupal\webform_submission_export_import\WebformSubmissionExportImportImporterInterface $importer
+   *   The webform submission importer.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, WebformRequestInterface $request_handler, WebformSubmissionGenerateInterface $submission_generate, WebformSubmissionExportImportImporterInterface $importer) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->webformSubmissionStorage = $entity_type_manager->getStorage('webform_submission');
+    $this->requestHandler = $request_handler;
+    $this->generate = $submission_generate;
+    $this->importer = $importer;
+
+    // Initialize the importer.
+    $webform = $this->requestHandler->getCurrentWebform();
+    $source_entity = $this->requestHandler->getCurrentSourceEntity();
+    $this->importer->setWebform($webform);
+    $this->importer->setSourceEntity($source_entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('webform.request'),
+      $container->get('webform_submission.generate'),
+      $container->get('webform_submission_export_import.importer')
+    );
+  }
+
+  /**
+   * Returns the Webform submission export example CSV view.
+   */
+  public function view() {
+    return $this->createResponse(FALSE);
+  }
+
+  /**
+   * Returns the Webform submission export example CSV download.
+   */
+  public function download() {
+    return $this->createResponse(TRUE);
+  }
+
+  /**
+   * Create a response containing submission CSV example.
+   *
+   * @param bool $download
+   *   TRUE is response should be downloaded.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   A response containing submission CSV example.
+   */
+  protected function createResponse($download = FALSE) {
+    $webform = $this->importer->getWebform();
+
+    // From: http://obtao.com/blog/2013/12/export-data-to-a-csv-file-with-symfony/
+    $response = new StreamedResponse(function () {
+      $handle = fopen('php://output', 'r+');
+
+      $header = $this->importer->exportHeader();
+      fputcsv($handle, $header);
+
+      for ($i = 1; $i <= 3; $i++) {
+        $webform_submission = $this->generateSubmission($i);
+        $record = $this->importer->exportSubmission($webform_submission);
+        fputcsv($handle, $record);
+      }
+
+      fclose($handle);
+    });
+
+    $response->headers->set('Content-Type', $download ? 'text/csv' : 'text/plain');
+    $response->headers->set('Content-Disposition', ($download ? 'attachment' : 'inline') . '; filename=' . $webform->id() . '.csv');
+    return $response;
+  }
+
+  /**
+   * Generate an unsaved webform submission.
+   *
+   * @param int $index
+   *   The submission's index used for the sid and serial number.
+   *
+   * @return \Drupal\webform\WebformSubmissionInterface
+   *   An unsaved webform submission.
+   */
+  protected function generateSubmission($index) {
+    $webform = $this->requestHandler->getCurrentWebform();
+    $source_entity = $this->requestHandler->getCurrentSourceEntity();
+
+    $users = $this->entityTypeManager->getStorage('user')->getQuery()->execute();
+    $uid = array_rand($users);
+
+    $url = $webform->toUrl();
+    if ($source_entity && $source_entity->hasLinkTemplate('canonical')) {
+      $url = $source_entity->toUrl();
+    }
+
+    return $this->webformSubmissionStorage->create([
+      'sid' => $index,
+      'serial' => $index,
+      'webform_id' => $webform->id(),
+      'entity_type' => ($source_entity) ? $source_entity->getEntityTypeId() : '',
+      'entity_id' => ($source_entity) ? $source_entity->id() : '',
+      'uid' => $uid,
+      'remote_addr' => mt_rand(0, 255) . '.' . mt_rand(0, 255) . '.' . mt_rand(0, 255) . '.' . mt_rand(0, 255),
+      'uri' => preg_replace('#^' . base_path() . '#', '/', $url->toString()),
+      'data' => Yaml::encode($this->generate->getData($webform)),
+      'created' => strtotime('-1 year'),
+      'completed' => rand(strtotime('-1 year'), time()),
+      'changed' => time(),
+    ]);
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_submission_export_import/src/Form/WebformSubmissionExportImportUploadForm.php b/web/modules/webform/modules/webform_submission_export_import/src/Form/WebformSubmissionExportImportUploadForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..da4a1af8ef9a809fbc8fb2504639415b8f0a8e51
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/src/Form/WebformSubmissionExportImportUploadForm.php
@@ -0,0 +1,727 @@
+<?php
+
+namespace Drupal\webform_submission_export_import\Form;
+
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\ConfirmFormHelper;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\Url;
+use Drupal\file\Entity\File;
+use Drupal\webform\Element\WebformMessage;
+use Drupal\webform\Utility\WebformOptionsHelper;
+use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformRequestInterface;
+use Drupal\webform_submission_export_import\WebformSubmissionExportImportImporterInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+
+/**
+ * Upload webform submission export import CSV.
+ */
+class WebformSubmissionExportImportUploadForm extends ConfirmFormBase {
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * Webform request handler.
+   *
+   * @var \Drupal\webform\WebformRequestInterface
+   */
+  protected $requestHandler;
+
+  /**
+   * The webform submission exporter.
+   *
+   * @var \Drupal\webform_submission_export_import\WebformSubmissionExportImportImporterInterface
+   */
+  protected $importer;
+
+  /**
+   * Constructs a WebformResultsExportController object.
+   *
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   * @param \Drupal\webform\WebformRequestInterface $request_handler
+   *   The webform request handler.
+   * @param \Drupal\webform_submission_export_import\WebformSubmissionExportImportImporterInterface $importer
+   *   The webform submission importer.
+   */
+  public function __construct(DateFormatterInterface $date_formatter, WebformRequestInterface $request_handler, WebformSubmissionExportImportImporterInterface $importer) {
+    $this->dateFormatter = $date_formatter;
+    $this->requestHandler = $request_handler;
+    $this->importer = $importer;
+
+    // Initialize the importer.
+    $webform = $this->requestHandler->getCurrentWebform();
+    $source_entity = $this->requestHandler->getCurrentSourceEntity();
+    $this->importer->setWebform($webform);
+    $this->importer->setSourceEntity($source_entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('date.formatter'),
+      $container->get('webform.request'),
+      $container->get('webform_submission_export_import.importer')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'webform_submission_export_import_upload_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormName() {
+    return 'webform_upload';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $this->setImportUri($form_state);
+    if (!$this->getImportUri()) {
+      return $this->buildUploadForm($form, $form_state);
+    }
+    else {
+      return $this->buildConfirmForm($form, $form_state);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Form submit handler is not being used.
+    // @see \Drupal\webform_submission_export_import\Form\WebformSubmissionExportImportUploadForm::buildUploadForm
+    // @see \Drupal\webform_submission_export_import\Form\WebformSubmissionExportImportUploadForm::buildConfirmForm
+  }
+
+  /****************************************************************************/
+  // Upload form.
+  /****************************************************************************/
+
+  /**
+   * Build upload form.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   An associative array containing the structure of the form.
+   */
+  protected function buildUploadForm(array $form, FormStateInterface $form_state) {
+    // Warning.
+    $form['experimental_warning'] = [
+      '#type' => 'webform_message',
+      '#message_type' => 'warning',
+      '#message_id' => 'webform_submission_export_import_experimental',
+      '#message_close' => TRUE,
+      '#message_storage' => WebformMessage::STORAGE_STATE,
+      '#message_message' => $this->t('Importing submissions is a new and experimental feature.') . '<br/><strong>' . $this->t('Please test and review your imported submissions using a development/test server.') . '</strong>',
+    ];
+
+    // Details.
+    $temporary_maximum_age = $this->config('system.file')->get('temporary_maximum_age');
+    $form['details'] = [
+      'title' => [
+        '#markup' => $this->t('Please note'),
+      ],
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => [
+          $this->t('All submission properties and data is optional.'),
+          $this->t('If UUIDs are included, existing submissions will always be updated.'),
+          $this->t('If UUIDs are not included, already imported and unchanged records will not create duplication submissions.'),
+          $this->t('File uploads must use publicly access URLs which begin with http:// or https://.'),
+          $this->t('Entity references can use UUIDs or entity IDs.'),
+          $this->t('Composite (single) values are annotated using double underscores. (e.g. ELEMENT_KEY__SUB_ELEMENT_KEY)'),
+          $this->t('Multiple values are comma delimited with any nested commas URI escaped (%2E).'),
+          $this->t('Multiple composite values are formatted using <a href=":href">inline YAML</a>.', [':href' => 'https://en.wikipedia.org/wiki/YAML#Basic_components']),
+          $this->t('Import maximum execution time limit is @time.', ['@time' => $this->dateFormatter->formatInterval($temporary_maximum_age)]),
+        ],
+      ],
+    ];
+
+    // Examples.
+    $download_url = $this->requestHandler->getCurrentWebformUrl('webform_submission_export_import.results_import.example.download');
+    $view_url = $this->requestHandler->getCurrentWebformUrl('webform_submission_export_import.results_import.example.view');
+    $t_args = [
+      ':href_download' => $download_url->toString(),
+      ':href_view' => $view_url->toString(),
+    ];
+    $form['examples'] = [
+      '#markup' => $this->t('<a href=":href_view">View</a> or <a href=":href_download">download</a> an example submission CSV.', $t_args),
+      '#prefix' => '<p>',
+      '#suffix' => '</p>',
+    ];
+
+    // Form.
+    $form['import'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Import data source'),
+      '#open' => TRUE,
+    ];
+    $form['import']['import_type'] = [
+      '#title' => 'Type',
+      '#type' => 'radios',
+      '#prefix' => '<div class="container-inline">',
+      '#suffix' => '</div>',
+      '#options' => [
+        'file' => $this->t('File upload'),
+        'url' => $this->t('Remote URL'),
+      ],
+      '#default_value' => 'file',
+    ];
+    $form['import']['import_file'] = [
+      '#type' => 'file',
+      '#title' => $this->t('Upload Submission CSV file'),
+      '#states' => [
+        'visible' => [
+          ':input[name="import_type"]' => ['value' => 'file'],
+        ],
+        'required' => [
+          ':input[name="import_type"]' => ['value' => 'file'],
+        ],
+      ],
+    ];
+    $form['import']['import_url'] = [
+      '#type' => 'url',
+      '#title' => $this->t('Enter Submission CSV remote URL'),
+      '#description' => $this->t('Remote URL could be a <a href=":href">published Google Sheet</a>.', [':href' => 'https://help.aftership.com/hc/en-us/articles/115008490908-CSV-Auto-Fetch-using-Google-Drive-Spreadsheet']),
+      '#states' => [
+        'visible' => [
+          ':input[name="import_type"]' => ['value' => 'url'],
+        ],
+        'required' => [
+          ':input[name="import_type"]' => ['value' => 'url'],
+        ],
+      ],
+    ];
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Continue'),
+      '#validate' => ['::validateUploadForm'],
+      '#submit' => ['::submitUploadForm'],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * Upload validation handler.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function validateUploadForm(array &$form, FormStateInterface $form_state) {
+    $import_type = $form_state->getValue('import_type');
+    switch ($import_type) {
+      case 'file':
+        $files = $this->getRequest()->files->get('files', []);
+        if (empty($files['import_file']) || !$files['import_file']->isValid()) {
+          $form_state->setErrorByName('import_file', $this->t('The file could not be uploaded.'));
+        }
+        break;
+
+      case 'url':
+        // @todo Determine if remote URL needs to be validated.
+        break;
+    }
+  }
+
+  /**
+   * Upload submission handler.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function submitUploadForm(array &$form, FormStateInterface $form_state) {
+    $validators = ['file_validate_extensions' => ['csv']];
+
+    $import_type = $form_state->getValue('import_type');
+
+    $file = NULL;
+    switch ($import_type) {
+      case 'file':
+        $files = file_save_upload('import_file', $validators);
+        $file = ($files) ? reset($files) : NULL;
+        break;
+
+      case 'url':
+        $import_url = $form_state->getValue('import_url');
+        $file_path = tempnam(file_directory_temp(), 'webform_submission_export_import_') . '.csv';
+        file_put_contents($file_path, file_get_contents($import_url));
+
+        $form_field_name = $this->t('Submission CSV (Comma Separated Values) file');
+        $file_size = filesize($file_path);
+        // Mimic Symfony and Drupal's upload file handling.
+        $file_info = new UploadedFile($file_path, basename($file_path), NULL, $file_size);
+        $file = _webform_submission_export_import_file_save_upload_single($file_info, $form_field_name, $validators);
+        break;
+    }
+
+    // If a managed file has been create to the file's id and rebuild the form.
+    if ($file) {
+      // Normalize carriage returns.
+      // This prevent issues with CSV files created in Excel.
+      $contents = file_get_contents($file->getFileUri());
+      $contents = preg_replace('~\R~u', "\r\n", $contents);
+      file_put_contents($file->getFileUri(), $contents);
+
+      $this->importer->setImportUri($file->getFileUri());
+      if ($this->importer->getTotal()) {
+        $form_state->set('import_fid', $file->id());
+        $form_state->setRebuild();
+      }
+      else {
+        $this->messenger()->addError($this->t("Uable to parse CSV file. Please review the CSV file's formatting."));
+      }
+    }
+  }
+
+  /****************************************************************************/
+  // Confirm form.
+  /****************************************************************************/
+
+  /**
+   * Build confirm import form.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   An associative array containing the structure of the form.
+   */
+  protected function buildConfirmForm(array $form, FormStateInterface $form_state) {
+    $import_options = $form_state->get('import_options');
+    $form['#disable_inline_form_errors'] = TRUE;
+    $form['#attributes']['class'][] = 'confirmation';
+    $form['#theme'] = 'confirm_form';
+    $form[$this->getFormName()] = ['#type' => 'hidden', '#value' => 1];
+
+    // Warning.
+    $total = $this->importer->getTotal();
+    $t_args = [
+      '@submissions' => $this->formatPlural($total, '1 submission', '@total submissions', ['@total' => $total]),
+    ];
+    $form['warning'] = [
+      '#type' => 'webform_message',
+      '#message_type' => 'warning',
+      '#message_message' => $this->t('Are you sure you want to import @submissions?', $t_args) . '<br/>' .
+        '<strong>' . $this->t('This action cannot be undone.') . '</strong>',
+    ];
+
+    // Details.
+    $actions = [
+      $this->t('Update submissions that have a corresponding UUID.'),
+      $this->t('Create new submissions.'),
+    ];
+    if ($import_options['skip_validation']) {
+      $actions[] = $this->t('Form validation will be skipped.');
+    }
+    else {
+      $actions[] = $this->t('Skip submissions that are invalid.');
+    }
+    $form['details'] = [
+      'title' => [
+        '#markup' => $this->t('This action will…'),
+      ],
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => $actions,
+      ],
+    ];
+
+    // Mapping.
+    $source = $this->appendNameToOptions($this->importer->getSourceColumns());
+    $destination = $this->appendNameToOptions($this->importer->getDestinationColumns());
+    $mappings = $this->importer->getSourceToDestinationColumnMapping();
+    $form['review'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Review import'),
+    ];
+    // Displaying when no UUID or token is found.
+    if (!isset($source['uuid']) && !isset($source['uuid'])) {
+      $form['review']['warning'] = [
+        '#type' => 'webform_message',
+        '#message_type' => 'warning',
+        '#message_message' => $this->t('No UUID or token was found in the source (CSV). A unique hash will be generated for the each CSV record. Any changes to already an imported record in the source (CSV) will create a new submission.', $t_args),
+        '#message_close' => TRUE,
+        '#message_storage' => WebformMessage::STORAGE_NONE,
+      ];
+    }
+    $form['review']['mapping'] = [
+      '#type' => 'webform_mapping',
+      '#title' => $this->t('Import mapping'),
+      '#source__title' => $this->t('Source (CSV)'),
+      '#destination__title' => $this->t('Destination (Submission)'),
+      '#description' => $this->t('Please review and select the imported CSV source column to destination element mapping'),
+      '#description_display' => 'before',
+      '#default_value' => $mappings,
+      '#required' => TRUE,
+      '#source' => $source,
+      '#destination' => $destination,
+      '#parents' => ['import_options', 'mapping'],
+    ];
+
+    // Options.
+    $form['import_options'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Import options'),
+      '#open' => TRUE,
+      '#tree' => TRUE,
+    ];
+    $form['import_options']['skip_validation'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Skip form validation'),
+      '#description' => $this->t('Skipping form validation can cause invalid data to be stored in the database.'),
+      '#return_value' => TRUE,
+    ];
+    $form['import_options']['treat_warnings_as_errors'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Treat all warnings as errors'),
+      '#description' => $this->t("CSV data that can't be converted to submission data will display a warning. If checked, these warnings will be treated as errors and prevent the submission from being created."),
+      '#return_value' => TRUE,
+    ];
+
+    // Confirm.
+    $form['confirm'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Yes, I want to import these submissions'),
+      '#required' => TRUE,
+    ];
+
+    // Actions.
+    $form['actions'] = ['#type' => 'actions'];
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->getConfirmText(),
+      '#button_type' => 'primary',
+      '#submit' => ['::submitImportForm'],
+    ];
+    $form['actions']['cancel'] = ConfirmFormHelper::buildCancelLink($this, $this->getRequest());
+    return $form;
+  }
+
+  /**
+   * Import submission handler.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function submitImportForm(array &$form, FormStateInterface $form_state) {
+    $this->setImportUri($form_state);
+
+    $webform = $this->requestHandler->getCurrentWebform();
+    $source_entity = $this->requestHandler->getCurrentSourceEntity();
+    $import_uri = $this->importer->getImportUri();
+    $import_options = $form_state->getValue('import_options');
+
+    $redirect_url = $this->requestHandler->getCurrentWebformUrl('webform.results_submissions');
+    $form_state->setRedirectUrl($redirect_url);
+
+    if ($this->importer->requiresBatch()) {
+      static::batchSet($webform, $source_entity, $import_uri, $import_options);
+    }
+    else {
+      $this->importer->setImportOptions($import_options);
+      $stats = $this->importer->import();
+      static::displayStats($stats);
+      $this->importer->deleteImportUri();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    // Do not alter the form's title.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Import');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return Url::fromRoute('<current>');
+  }
+
+  /****************************************************************************/
+  // Helper methods.
+  /****************************************************************************/
+
+  /**
+   * Set the CSV file URI.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state containing the upload file id.
+   */
+  protected function setImportUri(FormStateInterface $form_state) {
+    $fid = $form_state->get('import_fid');
+    if (!empty($fid)) {
+      $file = File::load($fid);
+      $this->importer->setImportUri($file->getFileUri());
+    }
+  }
+
+  /**
+   * Get the CSV file URI.
+   *
+   * @return string
+   *   The CSV file URI.
+   */
+  protected function getImportUri() {
+    return $this->importer->getImportUri();
+  }
+
+  /**
+   * Append option name to the displayed value.
+   *
+   * @param array $options
+   *   An array of options.
+   *
+   * @return array
+   *   An array of options with the option name appended to the displayed value.
+   */
+  protected function appendNameToOptions(array $options) {
+    foreach ($options as $name => $value) {
+      if ($name !== (string) $value) {
+        $options[$name] .= ' [' . $name . ']';
+      }
+    }
+    return $options;
+  }
+
+  /****************************************************************************/
+  // Batch functions.
+  // Using static method to prevent the service container from being serialized.
+  // "Prevents exception 'AssertionError' with message 'The container was serialized.'."
+  /****************************************************************************/
+
+  /**
+   * Batch API; Initialize batch operations.
+   *
+   * @param \Drupal\webform\WebformInterface|null $webform
+   *   A webform.
+   * @param \Drupal\Core\Entity\EntityInterface|null $source_entity
+   *   A webform source entity.
+   * @param string $import_uri
+   *   The URI of the CSV import file.
+   * @param array $import_options
+   *   An array of import options.
+   *
+   * @see http://www.jeffgeerling.com/blogs/jeff-geerling/using-batch-api-build-huge-csv
+   */
+  public static function batchSet(WebformInterface $webform, EntityInterface $source_entity = NULL, $import_uri = '', array $import_options = []) {
+    $parameters = [
+      $webform,
+      $source_entity,
+      $import_uri,
+      $import_options,
+    ];
+    $batch = [
+      'title' => t('Importing submissions'),
+      'init_message' => t('Initializing submission import'),
+      'error_message' => t('The import could not be completed because an error occurred.'),
+      'operations' => [
+        [['\Drupal\webform_submission_export_import\Form\WebformSubmissionExportImportUploadForm', 'batchProcess'], $parameters],
+      ],
+      'finished' => ['\Drupal\webform_submission_export_import\Form\WebformSubmissionExportImportUploadForm', 'batchFinish'],
+    ];
+
+    batch_set($batch);
+  }
+
+  /**
+   * Batch API callback; Write the header and rows of the export to the export file.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   The webform.
+   * @param \Drupal\Core\Entity\EntityInterface|null $source_entity
+   *   A webform source entity.
+   * @param string $import_uri
+   *   The URI of the CSV import file.
+   * @param array $import_options
+   *   An associative array of import options.
+   * @param mixed|array $context
+   *   The batch current context.
+   */
+  public static function batchProcess(WebformInterface $webform, EntityInterface $source_entity = NULL, $import_uri = '', array $import_options = [], &$context) {
+    /** @var \Drupal\webform_submission_export_import\WebformSubmissionExportImportImporterInterface $importer */
+    $importer = \Drupal::service('webform_submission_export_import.importer');
+    $importer->setWebform($webform);
+    $importer->setSourceEntity($source_entity);
+    $importer->setImportUri($import_uri);
+    $importer->setImportOptions($import_options);
+
+    if (empty($context['sandbox'])) {
+      $context['sandbox']['progress'] = 0;
+      $context['sandbox']['offset'] = 0;
+      $context['sandbox']['max'] = $importer->getTotal();
+      // Drush is losing the results so we are going to track theme
+      // via the sandbox.
+      $context['sandbox']['stats'] = [
+        'created' => 0,
+        'updated' => 0,
+        'skipped' => 0,
+        'total' => 0,
+        'warnings' => [],
+        'errors' => [],
+      ];
+      $context['results'] = [
+        'import_uri' => $import_uri,
+      ];
+    }
+
+    // Import CSV records.
+    $import_stats = $importer->import($context['sandbox']['offset'], $importer->getBatchLimit());
+
+    // Append import stats and errors to results.
+    foreach ($import_stats as $import_stat => $value) {
+      if (is_array($value)) {
+        // Convert translatable markup into strings to save memory.
+        $context['sandbox']['stats'][$import_stat] += WebformOptionsHelper::convertOptionsToString($value);
+      }
+      else {
+        $context['sandbox']['stats'][$import_stat] += $value;
+      }
+    }
+
+    // Track progress.
+    $context['sandbox']['progress'] += $import_stats['total'];
+    $context['sandbox']['offset'] += $importer->getBatchLimit();
+
+    // Display message.
+    $context['message'] = t('Imported @count of @total submissions…', ['@count' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']]);
+
+    // Track finished.
+    if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
+      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
+    }
+
+    // Context results are not being passed to batchFinish via Drush,
+    // therefor we are going to show them when this is finished.
+    if ($context['finished'] >= 1) {
+      static::displayStats($context['sandbox']['stats']);
+    }
+  }
+
+  /**
+   * Batch API callback; Completed export.
+   *
+   * @param bool $success
+   *   TRUE if batch successfully completed.
+   * @param array $results
+   *   Batch results.
+   * @param array $operations
+   *   An array of function calls (not used in this function).
+   */
+  public static function batchFinish($success, array $results, array $operations) {
+    if (!$success) {
+      \Drupal::messenger()->addStatus(t('Finished with an error.'));
+    }
+
+    // Delete import URI.
+    if (isset($results['import_uri'])) {
+      $files = \Drupal::entityTypeManager()
+        ->getStorage('file')
+        ->loadByProperties(['uri' => $results['import_uri']]);
+      foreach ($files as $file) {
+        $file->delete();
+      }
+    }
+  }
+
+  /**
+   * Disply import status.
+   *
+   * @param array $stats
+   *   Import stats.
+   */
+  public static function displayStats(array $stats) {
+    $is_cli = (PHP_SAPI === 'cli');
+    $number_of_errors = 0;
+    $error_limit = ($is_cli) ? NULL : 50;
+    $t_args = [
+      '@total' => $stats['total'],
+      '@created' => $stats['created'],
+      '@updated' => $stats['updated'],
+      '@skipped' => $stats['skipped'],
+    ];
+    if ($is_cli) {
+      \Drupal::logger('webform')->notice(t('Submission import completed. (total: @total; created: @created; updated: @updated; skipped: @skipped)', $t_args));
+    }
+    else {
+      \Drupal::messenger()->addStatus(t('Submission import completed. (total: @total; created: @created; updated: @updated; skipped: @skipped)', $t_args));
+    }
+    $message_types = [
+      'warnings' => MessengerInterface::TYPE_WARNING,
+      'errors' => MessengerInterface::TYPE_ERROR,
+    ];
+    foreach ($message_types as $message_group => $message_type) {
+      foreach ($stats[$message_group] as $row_number => $messages) {
+        $row_prefix = [
+          '#markup' => t('Row #@number', ['@number' => $row_number]),
+          '#prefix' => $is_cli ? '' : '<strong>',
+          '#suffix' => $is_cli ? ': ' : ':</strong> ',
+        ];
+        foreach ($messages as $message) {
+          if ($is_cli) {
+            $message = strip_tags($message);
+          }
+          $build = [
+            'row' => $row_prefix,
+            'message' => ['#markup' => $message],
+          ];
+          $message = \Drupal::service('renderer')->renderPlain($build);
+          if ($is_cli) {
+            \Drupal::logger('webform_submission_export_import')->$message_type($message);
+          }
+          else {
+            \Drupal::messenger()->addMessage($message, $message_type);
+          }
+          if ($error_limit && ++$number_of_errors >= $error_limit) {
+            return;
+          }
+        }
+      }
+    }
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_submission_export_import/src/Plugin/WebformExporter/WebformSubmissionExportImportWebformExporter.php b/web/modules/webform/modules/webform_submission_export_import/src/Plugin/WebformExporter/WebformSubmissionExportImportWebformExporter.php
new file mode 100644
index 0000000000000000000000000000000000000000..4e715b77483893c48bb9826398c8202e99ab62b6
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/src/Plugin/WebformExporter/WebformSubmissionExportImportWebformExporter.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Drupal\webform_submission_export_import\Plugin\WebformExporter;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Plugin\WebformExporter\FileHandleTraitWebformExporter;
+use Drupal\webform\Plugin\WebformExporterBase;
+use Drupal\webform\WebformSubmissionInterface;
+
+/**
+ * Defines a machine readable CSV export that can be imported back into the current webform.
+ *
+ * @WebformExporter(
+ *   id = "webform_submission_export_import",
+ *   label = @Translation("CSV download"),
+ *   description = @Translation("Exports results in CSV that can be imported back into the current webform."),
+ *   archive = FALSE,
+ *   files = FALSE,
+ *   options = FALSE,
+ * )
+ */
+class WebformSubmissionExportImportWebformExporter extends WebformExporterBase {
+
+  use FileHandleTraitWebformExporter;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return parent::defaultConfiguration() + [
+      'uuid' => TRUE,
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildConfigurationForm($form, $form_state);
+    $form['warning'] = [
+      '#type' => 'webform_message',
+      '#message_type' => 'warning',
+      '#message_message' => $this->t('<strong>Warning:</strong> Opening delimited text files with spreadsheet applications may expose you to <a href=":href">formula injection</a> or other security vulnerabilities. When the submissions contain data from untrusted users and the downloaded file will be used with Microsoft Excel, use \'HTML table\' format.', [':href' => 'https://www.google.com/search?q=spreadsheet+formula+injection']),
+    ];
+    $form['uuid'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Use UUIDs for all entity references.'),
+      '#description' => $this->t("If checked, all entity references will use the entity's UUID"),
+      '#return_value' => TRUE,
+      '#default_value' => $this->configuration['uuid'],
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFileExtension() {
+    return 'csv';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function writeHeader() {
+    $header = $this->getImporter()->exportHeader();
+    fputcsv($this->fileHandle, $header);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function writeSubmission(WebformSubmissionInterface $webform_submission) {
+    $record = $this->getImporter()->exportSubmission($webform_submission, $this->configuration);
+    fputcsv($this->fileHandle, $record);
+  }
+
+  /**
+   * Get the submission importer.
+   *
+   * @return \Drupal\webform_submission_export_import\WebformSubmissionExportImportImporterInterface
+   *   The submission importer.
+   */
+  protected function getImporter() {
+    /** @var \Drupal\webform_submission_export_import\WebformSubmissionExportImportImporterInterface $importer */
+    $importer = \Drupal::service('webform_submission_export_import.importer');
+    $importer->setWebform($this->getWebform());
+    return $importer;
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_submission_export_import/src/Routing/WebformSubmissionExportImportRouteSubscriber.php b/web/modules/webform/modules/webform_submission_export_import/src/Routing/WebformSubmissionExportImportRouteSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..1a03709a2cf3cd61aabf0069e49ca95713805981
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/src/Routing/WebformSubmissionExportImportRouteSubscriber.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\webform_submission_export_import\Routing;
+
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Routing\RouteSubscriberBase;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Remove webform submission import export routes.
+ */
+class WebformSubmissionExportImportRouteSubscriber extends RouteSubscriberBase {
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Constructs a WebformSubmissionLogRouteSubscriber object.
+   *
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(ModuleHandlerInterface $module_handler) {
+    $this->moduleHandler = $module_handler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function alterRoutes(RouteCollection $collection) {
+    if (!$this->moduleHandler->moduleExists('webform_node')) {
+      $collection->remove('entity.node.webform_submission_export_import.results_import');
+      $collection->remove('entity.node.webform_submission_export_import.results_import.example.download');
+      $collection->remove('entity.node.webform_submission_export_import.results_import.example.view');
+    }
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_submission_export_import/src/WebformSubmissionExportImportImporter.php b/web/modules/webform/modules/webform_submission_export_import/src/WebformSubmissionExportImportImporter.php
new file mode 100644
index 0000000000000000000000000000000000000000..fcec97da6f27d6a540c6fa2498c814fa104abf1f
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/src/WebformSubmissionExportImportImporter.php
@@ -0,0 +1,1172 @@
+<?php
+
+namespace Drupal\webform_submission_export_import;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\Serialization\Yaml;
+use Drupal\Core\Site\Settings;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\webform\Plugin\WebformElement\WebformCompositeBase;
+use Drupal\webform\Plugin\WebformElement\WebformLikert;
+use Drupal\webform\Plugin\WebformElement\WebformManagedFileBase;
+use Drupal\webform\Plugin\WebformElementEntityReferenceInterface;
+use Drupal\webform\Plugin\WebformElementManagerInterface;
+use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformSubmissionForm;
+use Drupal\webform\WebformSubmissionInterface;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+use Symfony\Component\Yaml\Dumper;
+
+/**
+ * Webform submission export import manager.
+ */
+class WebformSubmissionExportImportImporter implements WebformSubmissionExportImportImporterInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The configuration object factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The logger factory.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+   */
+  protected $loggerFactory;
+
+  /**
+   * The entity type manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Webform submission storage.
+   *
+   * @var \Drupal\webform\WebformSubmissionStorageInterface
+   */
+  protected $entityStorage;
+
+  /**
+   * Webform element manager.
+   *
+   * @var \Drupal\webform\Plugin\WebformElementManagerInterface
+   */
+  protected $elementManager;
+
+  /**
+   * The webform.
+   *
+   * @var \Drupal\webform\WebformInterface
+   */
+  protected $webform;
+
+  /**
+   * The source entity.
+   *
+   * @var \Drupal\Core\Entity\EntityInterface
+   */
+  protected $sourceEntity;
+
+  /**
+   * The import file URI.
+   *
+   * @var string
+   */
+  protected $importUri;
+
+  /**
+   * The total number of records being imported.
+   *
+   * @var int
+   */
+  protected $importTotal;
+
+  /**
+   * Import options.
+   *
+   * @var array
+   */
+  protected $importOptions;
+
+  /**
+   * An array containing webform element names.
+   *
+   * @var array
+   */
+  protected $elements;
+
+  /**
+   * An array containing a webform's field definition names.
+   *
+   * @var array
+   */
+  protected $fieldDefinitions;
+
+  /**
+   * Constructs a WebformSubmissionExportImport object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The configuration object factory.
+   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
+   *   The logger factory.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager
+   *   The webform element manager.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory, EntityTypeManagerInterface $entity_type_manager, WebformElementManagerInterface $element_manager) {
+    $this->configFactory = $config_factory;
+    $this->loggerFactory = $logger_factory;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->entityStorage = $entity_type_manager->getStorage('webform_submission');
+    $this->elementManager = $element_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setWebform(WebformInterface $webform = NULL) {
+    $this->webform = $webform;
+    $this->elementTypes = NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWebform() {
+    return $this->webform;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSourceEntity(EntityInterface $entity = NULL) {
+    $this->sourceEntity = $entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourceEntity() {
+    return $this->sourceEntity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getImportUri() {
+    return $this->importUri;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setImportUri($uri) {
+    $this->importUri = $uri;
+    $this->importTotal = NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteImportUri() {
+    $files = $this->entityTypeManager->getStorage('file')
+      ->loadByProperties(['uri' => $this->getImportUri()]);
+    if ($files) {
+      $file = reset($files);
+      $file->delete();
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getImportOptions() {
+    return $this->importOptions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setImportOptions(array $options) {
+    $this->importOptions = $options + $this->getDefaultImportOptions();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getImportOption($name) {
+    return $this->importOptions[$name];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultImportOptions() {
+    return [
+      'skip_validation' => FALSE,
+      'treat_warnings_as_errors' => FALSE,
+      'mapping' => [],
+    ];
+  }
+
+  /****************************************************************************/
+  // Webform field definitions and elements.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldDefinitions() {
+    if (isset($this->fieldDefinitions)) {
+      return $this->fieldDefinitions;
+    }
+
+    $this->fieldDefinitions = $this->entityStorage->getFieldDefinitions();
+    return $this->fieldDefinitions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getElements() {
+    if (isset($this->elements)) {
+      return $this->elements;
+    }
+
+    $this->elements = $this->getWebform()
+      ->getElementsInitializedFlattenedAndHasValue();
+    return $this->elements;
+  }
+
+  /****************************************************************************/
+  // Export.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function exportHeader() {
+    return array_keys($this->getDestinationColumns());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function exportSubmission(WebformSubmissionInterface $webform_submission, array $export_options = []) {
+    $submission_data = $webform_submission->toArray(TRUE);
+
+    $record = [];
+
+    // Append fields.
+    $field_definitions = $this->getFieldDefinitions();
+    foreach ($field_definitions as $field_name => $field_definition) {
+      switch ($field_name) {
+        case 'uid':
+          $value = $this->getEntityExportId($webform_submission->getOwner(), $export_options);
+          break;
+
+        case 'entity_id':
+          $value = $this->getEntityExportId($webform_submission->getSourceEntity(), $export_options);
+          break;
+
+        default:
+          $value = (isset($submission_data[$field_name])) ? $submission_data[$field_name] : '';
+          break;
+      }
+      $record[] = $this->exportValue($value);
+    }
+
+    // Append elements.
+    $elements = $this->getElements();
+    foreach ($elements as $element) {
+      $element_plugin = $this->elementManager->getElementInstance($element);
+      $has_multiple_values = $element_plugin->hasMultipleValues($element);
+      if ($element_plugin instanceof WebformManagedFileBase) {
+        // Files: Get File URLS.
+        /** @var \Drupal\file\FileInterface $files */
+        $files = $element_plugin->getTargetEntities($element, $webform_submission) ?: [];
+        $values = [];
+        foreach ($files as $file) {
+          $values[] = file_create_url($file->getFileUri());
+        }
+        $value = implode(',', $values);
+        $record[] = $this->exportValue($value);
+      }
+      elseif ($element_plugin instanceof WebformElementEntityReferenceInterface) {
+        // Entity references: Get entity UUIDs.
+        $entities = $element_plugin->getTargetEntities($element, $webform_submission);
+        $values = [];
+        foreach ($entities as $entity) {
+          $values[] = $this->getEntityExportId($entity, $export_options);
+        }
+        $value = implode(',', $values);
+        $record[] = $this->exportValue($value);
+      }
+      elseif ($element_plugin instanceof WebformLikert) {
+        // Single Composite: Split questions into individual columns.
+        $value = $element_plugin->getValue($element, $webform_submission);
+        $question_keys = array_keys($element['#questions']);
+        foreach ($question_keys as $question_key) {
+          $question_value = (isset($value[$question_key])) ? $value[$question_key] : '';
+          $record[] = $this->exportValue($question_value);
+        }
+      }
+      elseif ($element_plugin instanceof WebformCompositeBase && !$has_multiple_values) {
+        // Composite: Split single composite sub elements into individual columns.
+        $value = $element_plugin->getValue($element, $webform_submission);
+        $composite_element_keys = array_keys($element_plugin->getCompositeElements());
+        foreach ($composite_element_keys as $composite_element_key) {
+          $composite_value = (isset($value[$composite_element_key])) ? $value[$composite_element_key] : '';
+          $record[] = $this->exportValue($composite_value);
+        }
+      }
+      elseif ($element_plugin->isComposite()) {
+        // Composite: Convert multiple composite values to a single line of YAML.
+        $value = $element_plugin->getValue($element, $webform_submission);
+        $dumper = new Dumper(2);
+        $record[] = $dumper->dump($value);
+      }
+      elseif ($has_multiple_values) {
+        // Multiple: Convert to comma separated values with commas URL encodes.
+        $values = $element_plugin->getValue($element, $webform_submission);
+        $values = ($values !== NULL) ? (array) $values : [];
+        foreach ($values as $index => $value) {
+          $values[$index] = str_replace(',', '%2C', $value);
+        }
+        $value = implode(',', $values);
+        $record[] = $this->exportValue($value);
+      }
+      else {
+        // Default: Convert NULL values to empty strings.
+        $value = $element_plugin->getValue($element, $webform_submission);
+        $value = ($value !== NULL) ? $value : '';
+        $record[] = $this->exportValue($value);
+      }
+    }
+
+    return $record;
+  }
+
+  /****************************************************************************/
+  // Import.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function import($offset = 0, $limit = NULL) {
+    if ($limit === NULL) {
+      $limit = $this->getBatchLimit();
+    }
+
+    $import_options = $this->getImportOptions();
+
+    // Open CSV file.
+    $handle = fopen($this->getImportUri(), 'r');
+
+    // Get the column names.
+    $column_names = fgetcsv($handle);
+    foreach ($column_names as $index => $name) {
+      $column_names[$index] = $name;
+    }
+
+    // Fast forward CSV file to offset.
+    $index = 0;
+    while ($index < $offset && !feof($handle)) {
+      fgets($handle);
+      $index++;
+    }
+
+    // Collect import stats.
+    $stats = [
+      'created' => 0,
+      'updated' => 0,
+      'skipped' => 0,
+      'total' => 0,
+      'warnings' => [],
+      'errors' => [],
+    ];
+
+    // Import submission records.
+    while ($stats['total'] < $limit && !feof($handle)) {
+      // Get CSV values.
+      $values = fgetcsv($handle);
+      // Complete ignored empty rows.
+      if (empty($values)) {
+        continue;
+      }
+
+      // Create CSV record.
+      $record = array_combine($column_names, $values);
+      // Trim all values.
+      foreach ($record as $key => $value) {
+        $record[$key] = trim($value);
+      }
+
+      $index++;
+      $stats['total']++;
+
+      // Track row specific warnings and errors.
+      $stats['warnings'][$index] = [];
+      $stats['errors'][$index] = [];
+      $row_warnings =& $stats['warnings'][$index];
+      $row_errors =& $stats['errors'][$index];
+
+      // Track original record.
+      $original_record = $record;
+
+      // Map.
+      $record = $this->importMapRecord($record);
+
+      // Token: Generate token from the original CSV record.
+      if (empty($record['token'])) {
+        $record['token'] = md5(Settings::getHashSalt() . serialize($original_record));
+      }
+
+      // Prepare.
+      $webform_submission = $this->importLoadSubmission($record);
+      if ($errors = $this->importPrepareRecord($record, $webform_submission)) {
+        if (!empty($import_options['treat_warnings_as_errors'])) {
+          $row_errors = array_merge($row_warnings, array_values($errors));
+        }
+        else {
+          $row_warnings = array_merge($row_warnings, array_values($errors));
+        }
+      }
+
+      // Validate.
+      if (empty($import_options['skip_validation'])) {
+        if ($errors = $this->importValidateRecord($record)) {
+          $row_errors = array_merge($row_errors, array_values($errors));
+        }
+      }
+
+      // Skip import if there are row errors.
+      if ($row_errors) {
+        $stats['skipped']++;
+        continue;
+      }
+
+      // Save.
+      $this->importSaveSubmission($record, $webform_submission);
+      $stats[$webform_submission ? 'updated' : 'created']++;
+    }
+
+    fclose($handle);
+    return $stats;
+  }
+
+  /**
+   * Map source (CSV) record to destination (submission) records.
+   *
+   * @param array $record
+   *   The source (CSV) record.
+   *
+   * @return array
+   *   The destination (submission) records.
+   */
+  protected function importMapRecord(array $record) {
+    $mapping = $this->getImportOption('mapping');
+
+    // If not mapping is defined return the record AS-IS.
+    if (empty($mapping)) {
+      return $record;
+    }
+
+    $mapped_record = [];
+    foreach ($mapping as $source_name => $destination_name) {
+      if (isset($record[$source_name])) {
+        $mapped_record[$destination_name] = $record[$source_name];
+      }
+    }
+    return $mapped_record;
+  }
+
+  /**
+   * Load import submission record via UUID or token.
+   *
+   * @param array $record
+   *   The import submission record.
+   *
+   * @return \Drupal\webform\WebformSubmissionInterface|null
+   *   The existing webform submission or NULL if no existing submission found.
+   */
+  protected function importLoadSubmission(array &$record) {
+    $unique_keys = ['uuid', 'token'];
+    foreach ($unique_keys as $unique_key) {
+      if (!empty($record[$unique_key])) {
+        if ($webform_submissions = $this->entityStorage->loadByProperties([$unique_key => $record[$unique_key]])) {
+          return reset($webform_submissions);
+        }
+      }
+    }
+    return NULL;
+  }
+
+  /**
+   * Prepare import submission record.
+   *
+   * @param array $record
+   *   The import submission record.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   The existing webform submission.
+   *
+   * @return array
+   *   An array of error messages.
+   */
+  protected function importPrepareRecord(array &$record, WebformSubmissionInterface $webform_submission = NULL) {
+    // Track errors.
+    $errors = [];
+
+    if (isset($record['uid'])) {
+      // Convert user id to internal IDs.
+      $record['uid'] = $this->getEntityImportId('user', $record['uid']);
+      // Convert empty uid to anonymous user (UID: 0).
+      if (empty($record['uid'])) {
+        $record['uid'] = 0;
+      }
+    }
+
+    // Remove empty uuid.
+    if (empty($record['uuid'])) {
+      unset($record['uuid']);
+    }
+
+    $webform = $this->getWebform();
+    $source_entity = $this->getSourceEntity();
+
+    // Set webform id.
+    $record['webform_id'] = $webform->id();
+
+    // Set source entity.
+    // Load or convert the source entity id to an internal ID.
+    if ($source_entity) {
+      $record['entity_type'] = $source_entity->getEntityTypeId();
+      $record['entity_id'] = $source_entity->id();
+    }
+    elseif (!empty($record['entity_type']) && isset($record['entity_id'])) {
+      $record['entity_id'] = $this->getEntityImportId($record['entity_type'], $record['entity_id']);
+      // If source entity_id can't be found, log error, and
+      // remove the source  entity_type.
+      if ($record['entity_id'] === NULL) {
+        $t_args = [
+          '@entity_type' => $record['entity_type'],
+          '@entity_id' => $record['entity_id'],
+        ];
+        $errors[] = $this->t('Unable to locate source entity (@entity_type:@entity_id)', $t_args);
+        $record['entity_type'] = NULL;
+      }
+    }
+
+    // Convert record to submission element data.
+    $elements = $this->getElements();
+    foreach ($record as $name => $value) {
+      // Set record value form an element.
+      if (isset($elements[$name])) {
+        $element = $elements[$name];
+        $record[$name] = $this->importElement($element, $value, $webform_submission, $errors);
+        continue;
+      }
+
+      // Check if record name is a composite element which is
+      // delimited using '__'.
+      if (strpos($name, '__') === FALSE) {
+        continue;
+      }
+
+      // Get element and composite key and confirm that the element exists.
+      list($element_key, $composite_key) = explode('__', $name);
+      if (!isset($elements[$element_key])) {
+        continue;
+      }
+
+      // Make sure the composite element is not storing multiple values which
+      // must use YAML.
+      // @see \Drupal\webform_submission_export_import\WebformSubmissionExportImportImporter::importCompositeElement
+      $element = $elements[$element_key];
+      $element_plugin = $this->elementManager->getElementInstance($element);
+      if ($element_plugin->hasMultipleValues($element)) {
+        continue;
+      }
+
+      if ($element_plugin instanceof WebformLikert) {
+        // Make sure the Likert question exists.
+        if (!isset($element['#questions']) || !isset($element['#questions'][$composite_key])) {
+          continue;
+        }
+
+        $record[$element_key][$composite_key] = $value;
+      }
+      elseif ($element_plugin instanceof WebformCompositeBase) {
+        // Get the the composite element element and make sure it exists.
+        $composite_elements = $element_plugin->getCompositeElements();
+        if (!isset($composite_elements[$composite_key])) {
+          continue;
+        }
+
+        $composite_element = $composite_elements[$composite_key];
+        $record[$element_key][$composite_key] = $this->importElement($composite_element, $value, $webform_submission, $errors);
+      }
+    }
+
+    return $errors;
+  }
+
+  /**
+   * Import element.
+   *
+   * @param array $element
+   *   A managed file element.
+   * @param mixed $value
+   *   File URI(s) from CSV record.
+   * @param \Drupal\webform\WebformSubmissionInterface|null $webform_submission
+   *   Existing submission or NULL if new submission.
+   * @param array $errors
+   *   An array of error messages.
+   *
+   * @return array|int|null
+   *   An array of multiple files, single file id, or NULL if file could
+   *   not be imported.
+   */
+  protected function importElement(array $element, $value, WebformSubmissionInterface $webform_submission = NULL, array &$errors) {
+    $element_plugin = $this->elementManager->getElementInstance($element);
+
+    if ($value === '') {
+      // Empty: Convert multiple values to an empty array or NULL value.
+      return ($element_plugin->hasMultipleValues($element)) ? [] : NULL;
+    }
+    elseif ($element_plugin instanceof WebformManagedFileBase) {
+      // Files: Convert File URL to file object.
+      return $this->importManageFileElement($element, $value, $webform_submission, $errors);
+    }
+    elseif ($element_plugin instanceof WebformElementEntityReferenceInterface) {
+      // Entity references: Convert entity UUIDs to internal IDs.
+      return $this->importEntityReferenceElement($element, $value, $webform_submission, $errors);
+    }
+    elseif ($element_plugin->isComposite()) {
+      // Composite: Decode YAML.
+      return $this->importCompositeElement($element, $value, $webform_submission, $errors);
+    }
+    elseif ($element_plugin->hasMultipleValues($element)) {
+      // Multiple: Convert to comma separated values to array.
+      return $this->importMultipleElement($element, $value, $webform_submission, $errors);
+    }
+    else {
+      return $value;
+    }
+  }
+
+  /**
+   * Import managed file element.
+   *
+   * @param array $element
+   *   A managed file element.
+   * @param mixed $value
+   *   File URI(s) from CSV record.
+   * @param \Drupal\webform\WebformSubmissionInterface|null $webform_submission
+   *   Existing submission or NULL if new submission.
+   * @param array $errors
+   *   An array of error messages.
+   *
+   * @return array|int|null
+   *   An array of multiple files, single file id, or NULL if file could
+   *   not be imported.
+   */
+  protected function importManageFileElement(array $element, $value, WebformSubmissionInterface $webform_submission = NULL, array &$errors) {
+    $webform = $this->getWebform();
+    $element_plugin = $this->elementManager->getElementInstance($element);
+
+    // Prepare managed file element with a temp submission.
+    $element_plugin->prepare($element, $this->entityStorage->create(['webform_id' => $webform->id()]));
+
+    // Get file destination.
+    $file_destination = isset($element['#upload_location']) ? $element['#upload_location'] : NULL;
+    if (isset($file_destination) && !file_prepare_directory($file_destination, FILE_CREATE_DIRECTORY)) {
+      $this->loggerFactory->get('file')
+        ->notice('The upload directory %directory for the file element %name could not be created or is not accessible. A newly uploaded file could not be saved in this directory as a consequence, and the upload was canceled.', [
+          '%directory' => $file_destination,
+          '%name' => $element['#webform_key'],
+        ]);
+      return ($element_plugin->hasMultipleValues($element)) ? [] : NULL;
+    }
+
+    // Get file upload validators.
+    $file_upload_validators = $element['#upload_validators'];
+
+    // Create a uri and sha1 lookup tables for existing files.
+    $existing_file_ids = [];
+    $existing_file_uris = [];
+    $existing_files = ($webform_submission) ? $element_plugin->getTargetEntities($element, $webform_submission) ?: [] : [];
+    foreach ($existing_files as $existing_file) {
+      $existing_file_uri = file_create_url($existing_file->getFileUri());
+      $existing_file_uris[$existing_file_uri] = $existing_file->id();
+
+      $existing_file_hash = sha1_file($existing_file->getFileUri());
+      $existing_file_ids[$existing_file_hash] = $existing_file->id();
+    }
+
+    // Find or upload new file.
+    $new_file_ids = [];
+    $new_file_uris = explode(',', $value);
+    foreach ($new_file_uris as $new_file_uri) {
+      // Check existing file URIs.
+      if (isset($existing_file_uris[$new_file_uri])) {
+        $new_file_ids[$new_file_uri] = $existing_file_uris[$new_file_uri];
+        continue;
+      }
+
+      $t_args = [
+        '@element_key' => $element['#webform_key'],
+        '@url' => $new_file_uri,
+      ];
+
+      // Check URL protocol.
+      if (!preg_match('#^https?://#', $new_file_uri)) {
+        $errors[] = $this->t('[@element_key] Invalid file URL (@url). URLS must begin with http:// or https://.', $t_args);
+        continue;
+      }
+
+      // Check URL status code.
+      $file_headers = @get_headers($new_file_uri);
+      if (!$file_headers || $file_headers[0] == 'HTTP/1.1 404 Not Found') {
+        $errors[] = $this->t('[@element_key] URL (@url) returns 404 file not found.', $t_args);
+        continue;
+      }
+
+      $new_file_hash = @sha1_file($new_file_uri);
+      if (!$new_file_hash) {
+        $errors[] = $this->t('[@element_key] Unable to read file from URL (@url).', $t_args);
+        continue;
+      }
+
+      // Check existing file hashes.
+      if (isset($existing_file_ids[$new_file_hash])) {
+        $new_file_ids[$new_file_hash] = $existing_file_ids[$new_file_hash];
+        continue;
+      }
+
+      // Write new file URI to server and upload it.
+      $temp_file_contents = @file_get_contents($new_file_uri);
+      if (!$temp_file_contents) {
+        $errors[] = $this->t('[@element_key] Unable to read file from URL (@url).', $t_args);
+        continue;
+      }
+
+      // Create a temp file.
+      $handle = tmpfile();
+      fwrite($handle, $temp_file_contents);
+      $temp_file_meta_data = stream_get_meta_data($handle);
+      $temp_file_path = $temp_file_meta_data['uri'];
+      $temp_file_size = filesize($temp_file_path);
+
+      // Mimic Symfony and Drupal's upload file handling.
+      $temp_file_info = new UploadedFile($temp_file_path, basename($new_file_uri), NULL, $temp_file_size);
+      $webform_element_key = $element_plugin->getLabel($element);
+      $new_file = _webform_submission_export_import_file_save_upload_single($temp_file_info, $webform_element_key, $file_upload_validators, $file_destination);
+      if ($new_file) {
+        $new_file_ids[$new_file_hash] = $new_file->id();
+      }
+    }
+
+    $values = array_values($new_file_ids);
+    if (empty($values)) {
+      return ($element_plugin->hasMultipleValues($element)) ? [] : NULL;
+    }
+    else {
+      return ($element_plugin->hasMultipleValues($element)) ? $values : reset($values);
+    }
+  }
+
+  /**
+   * Import entity reference element.
+   *
+   * @param array $element
+   *   A managed file element.
+   * @param mixed $value
+   *   File URI(s) from CSV record.
+   * @param \Drupal\webform\WebformSubmissionInterface|null $webform_submission
+   *   Existing submission or NULL if new submission.
+   * @param array $errors
+   *   An array of error messages.
+   *
+   * @return array|int|null
+   *   An array of entity ids, a single entity id, or NULL if entity ids
+   *   could not be imported.
+   */
+  protected function importEntityReferenceElement(array $element, $value, WebformSubmissionInterface $webform_submission = NULL, array &$errors) {
+    $element_plugin = $this->elementManager->getElementInstance($element);
+    $entity_type_id = $element_plugin->getTargetType($element);
+    $values = explode(',', $value);
+    foreach ($values as $index => $value) {
+      $values[$index] = $this->getEntityImportId($entity_type_id, $value);
+      if ($values[$index] === NULL) {
+        $t_args = [
+          '@element_key' => $element['#webform_key'],
+          '@entity_id' => $value,
+        ];
+        $errors[] = $this->t('[@element_key] Unable to locate entity (@entity_id).', $t_args);
+        unset($values[$index]);
+      }
+    }
+    $values = array_values($values);
+    if (empty($values)) {
+      return ($element_plugin->hasMultipleValues($element)) ? [] : NULL;
+    }
+    else {
+      return ($element_plugin->hasMultipleValues($element)) ? $values : reset($values);
+    }
+  }
+
+  /**
+   * Import composite element.
+   *
+   * @param array $element
+   *   A composite element.
+   * @param mixed $value
+   *   File URI(s) from CSV record.
+   * @param \Drupal\webform\WebformSubmissionInterface|null $webform_submission
+   *   Existing submission or NULL if new submission.
+   * @param array $errors
+   *   An array of error messages.
+   *
+   * @return array
+   *   An array of composite element data.
+   */
+  protected function importCompositeElement(array $element, $value, WebformSubmissionInterface $webform_submission = NULL, array &$errors) {
+    try {
+      return Yaml::decode($value);
+    }
+    catch (\Exception $exception) {
+      $t_args = [
+        '@element_key' => $element['#webform_key'],
+        '@error' => $exception->getMessage(),
+      ];
+      $errors[] = $this->t('[@element_key] YAML is not valid. @error', $t_args);
+      return [];
+    }
+  }
+
+  /**
+   * Import multiple element.
+   *
+   * @param array $element
+   *   An element with multiple values..
+   * @param mixed $value
+   *   File URI(s) from CSV record.
+   * @param \Drupal\webform\WebformSubmissionInterface|null $webform_submission
+   *   Existing submission or NULL if new submission.
+   * @param array $errors
+   *   An array of error messages.
+   *
+   * @return array
+   *   An array of multiple values.
+   */
+  protected function importMultipleElement(array $element, $value, WebformSubmissionInterface $webform_submission = NULL, array &$errors) {
+    $values = preg_split('/\s*,\s*/', $value);
+    foreach ($values as $index => $item) {
+      $values[$index] = str_replace('%2C', ',', $item);
+    }
+    return $values;
+  }
+
+  /**
+   * Validate import record submission.
+   *
+   * @param array $record
+   *   The record to be imported.
+   *
+   * @return array
+   *   An array of error messages.
+   */
+  protected function importValidateRecord(array $record) {
+    $values = $this->importConvertRecordToValues($record);
+    return WebformSubmissionForm::validateFormValues($values);
+  }
+
+  /**
+   * Save import record submission.
+   *
+   * @param array $record
+   *   The record to be imported.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   The existing webform submission.
+   */
+  protected function importSaveSubmission(array $record, WebformSubmissionInterface $webform_submission = NULL) {
+    $field_definitions = $this->getFieldDefinitions();
+    $elements = $this->getElements();
+
+    if ($webform_submission) {
+      // Update submission.
+      unset($record['sid'], $record['serial'], $record['uuid']);
+      foreach ($record as $name => $value) {
+        if (isset($field_definitions[$name])) {
+          $webform_submission->set($name, $value);
+        }
+        elseif (isset($elements[$name])) {
+          $webform_submission->setElementData($name, $value);
+        }
+      }
+    }
+    else {
+      // Create submission.
+      unset($record['sid'], $record['serial']);
+      $values = $this->importConvertRecordToValues($record);
+      $webform_submission = $this->entityStorage->create($values);
+    }
+    $webform_submission->save();
+  }
+
+  /**
+   * Convert CSV records to entity values.
+   *
+   * @param array $record
+   *   The record to be imported.
+   *
+   * @return array
+   *   The CSV records converted to entity values.
+   *
+   * @see \Drupal\webform\Entity\WebformSubmission::preCreate
+   */
+  protected function importConvertRecordToValues(array $record) {
+    $field_definitions = $this->getFieldDefinitions();
+    $elements = $this->getElements();
+
+    $values = ['data' => []];
+    foreach ($record as $name => $value) {
+      if (isset($field_definitions[$name])) {
+        $values[$name] = $value;
+      }
+      elseif (isset($elements[$name])) {
+        $values['data'][$name] = $value;
+      }
+    }
+
+    // Never allow the record to set the sid or serial.
+    unset($values['sid'], $values['serial']);
+
+    return $values;
+  }
+
+  /****************************************************************************/
+  // Summary.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTotal() {
+    if (isset($this->importTotal)) {
+      return $this->importTotal;
+    }
+    // Ignore the header.
+    $total = -1;
+    $handle = fopen($this->importUri, 'r');
+    while (!feof($handle)) {
+      $line = fgets($handle);
+      if (!empty(trim($line))) {
+        $total++;
+      }
+    }
+    $this->importTotal = $total;
+    return $this->importTotal;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourceColumns() {
+    $file = fopen($this->getImportUri(), 'r');
+    $values = fgetcsv($file);
+    fclose($file);
+    return array_combine($values, $values);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDestinationColumns() {
+    $columns = [];
+
+    $field_definitions = $this->getFieldDefinitions();
+    foreach ($field_definitions as $field_name => $field_definition) {
+      $columns[$field_name] = $field_definition['title'];
+    }
+
+    $elements = $this->getElements();
+    foreach ($elements as $element_key => $element) {
+      $element_plugin = $this->elementManager->getElementInstance($element);
+      $element_title = $element_plugin->getAdminLabel($element);
+      $has_multiple_values = $element_plugin->hasMultipleValues($element);
+      if (!$has_multiple_values && $element_plugin instanceof WebformCompositeBase) {
+        $composite_elements = $element_plugin->getCompositeElements();
+        foreach ($composite_elements as $composite_element_key => $composite_element) {
+          $composite_element_name = $element_key . '__' . $composite_element_key;
+          $composite_element_plugin = $this->elementManager->getElementInstance($composite_element);
+          $composite_element_title = $composite_element_plugin->getAdminLabel($composite_element);
+          $t_args = [
+            '@element_title' => $element_title,
+            '@composite_title' => $composite_element_title,
+          ];
+          $columns[$composite_element_name] = $this->t('@element_title: @composite_title', $t_args);
+        }
+      }
+      elseif (!$has_multiple_values && $element_plugin instanceof WebformLikert) {
+        $questions = $element['#questions'];
+        foreach ($questions as $question_key => $question) {
+          $question_element_name = $element_key . '__' . $question_key;
+          $t_args = [
+            '@element_title' => $element_title,
+            '@question_title' => $question,
+          ];
+          $columns[$question_element_name] = $this->t('@element_title: @question_title', $t_args);
+        }
+      }
+      else {
+        $columns[$element_key] = $element_title;
+      }
+    }
+
+    return $columns;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourceToDestinationColumnMapping() {
+    $source_column_names = $this->getSourceColumns();
+    $destination_column_names = $this->getDestinationColumns();
+
+    // Map source to destination columns.
+    $mapping = [];
+    foreach ($source_column_names as $source_column_name) {
+      if (isset($destination_column_names[$source_column_name])) {
+        $mapping[$source_column_name] = $source_column_name;
+      }
+      else {
+        $mapping[$source_column_name] = '';
+      }
+    }
+
+    return $mapping;
+  }
+
+  /****************************************************************************/
+  // Batch.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBatchLimit() {
+    return $this->configFactory->get('webform.settings')
+      ->get('batch.default_batch_import_size') ?: 100;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function requiresBatch() {
+    return ($this->getTotal() > $this->getBatchLimit()) ? TRUE : FALSE;
+  }
+
+  /****************************************************************************/
+  // Helpers.
+  /****************************************************************************/
+
+  /**
+   * Get an entity's export id or UUID based on the export options.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   An entity.
+   * @param array $export_options
+   *   Export options.
+   *
+   * @return string|int
+   *   The entity's id or UUID.
+   */
+  protected function getEntityExportId(EntityInterface $entity = NULL, array $export_options = []) {
+    if (!$entity) {
+      return '';
+    }
+    else {
+      return (empty($export_options['uuid'])) ? $entity->id() : $entity->uuid();
+    }
+  }
+
+  /**
+   * Get an entity's import internal id.
+   *
+   * @param string $entity_type
+   *   The entity type.
+   * @param string $entity_id
+   *   The entity id or UUID.
+   *
+   * @return int|string|null
+   *   An entity's internal id. NULL if an entity's internal id
+   *   can't be determined.
+   */
+  protected function getEntityImportId($entity_type, $entity_id) {
+    if (!$this->entityTypeManager->hasDefinition($entity_type)) {
+      return NULL;
+    }
+
+    $entity_storage = $this->entityTypeManager->getStorage($entity_type);
+
+    // Load entity by properties.
+    if ($entity_type === 'user') {
+      $properties = ['uuid', 'mail', 'name'];
+    }
+    else {
+      $properties = ['uuid'];
+    }
+    foreach ($properties as $property) {
+      $entities = $entity_storage->loadByProperties([$property => $entity_id]);
+      if ($entities) {
+        $entity = reset($entities);
+        return $entity->id();
+      }
+    }
+
+    // Load entity by internal id.
+    $entity = $entity_storage->load($entity_id);
+    if ($entity) {
+      return $entity->id();
+    }
+
+    return NULL;
+  }
+
+  /**
+   * Export value so that it can be editted in Excel and Google Sheets.
+   *
+   * @param string $value
+   *   A value.
+   *
+   * @return string
+   *   A value that it can be editted in Excel and Googl Sheets.
+   */
+  protected function exportValue($value) {
+    // Prevent Excel and Google Sheets from convert string beginning with
+    // + or - into formulas by adding a space before the string.
+    // @see https://stackoverflow.com/questions/4438589/bypass-excel-csv-formula-conversion-on-fields-starting-with-or
+    if (is_string($value) && strpos($value, '+') === 0 || strpos($value, '-') === 0) {
+      return ' ' . $value;
+    }
+    else {
+      return $value;
+    }
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_submission_export_import/src/WebformSubmissionExportImportImporterInterface.php b/web/modules/webform/modules/webform_submission_export_import/src/WebformSubmissionExportImportImporterInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..89beedfb36196cee0e8b83987d50ebff48706583
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/src/WebformSubmissionExportImportImporterInterface.php
@@ -0,0 +1,226 @@
+<?php
+
+namespace Drupal\webform_submission_export_import;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformSubmissionInterface;
+
+/**
+ * Defines an interface for exporting webform submission results.
+ */
+interface WebformSubmissionExportImportImporterInterface {
+
+  /**
+   * Set the webform whose submissions are being imported.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   */
+  public function setWebform(WebformInterface $webform = NULL);
+
+  /**
+   * Get the webform whose submissions are being imported.
+   *
+   * @return \Drupal\webform\WebformInterface
+   *   A webform.
+   */
+  public function getWebform();
+
+  /**
+   * Set the webform source entity whose submissions are being imported.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   A webform's source entity.
+   */
+  public function setSourceEntity(EntityInterface $entity = NULL);
+
+  /**
+   * Get the webform source entity whose submissions are being imported.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   A webform's source entity.
+   */
+  public function getSourceEntity();
+
+  /**
+   * Get the URI of the CSV import file.
+   *
+   * @return string
+   *   The URI of the CSV import file.
+   */
+  public function getImportUri();
+
+  /**
+   * Set the URI of the CSV import file.
+   *
+   * @param string $uri
+   *   The URI of the CSV import file.
+   */
+  public function setImportUri($uri);
+
+  /**
+   * Attempt delete managed file created for import uri.
+   *
+   * @return bool
+   *   TRUE  if managed file created for import uri was deleted.
+   */
+  public function deleteImportUri();
+
+  /**
+   * Get import options.
+   *
+   * @return array
+   *   Import options.
+   */
+  public function getImportOptions();
+
+  /**
+   * Set import options.
+   *
+   * @param array $options
+   *   Import options.
+   */
+  public function setImportOptions(array $options);
+
+  /**
+   * Get import option value.
+   *
+   * @param string $name
+   *   The import option name.
+   *
+   * @return mixed
+   *   The import option value or the default value.
+   */
+  public function getImportOption($name);
+
+  /**
+   * Get default import options.
+   *
+   * @return array
+   *   Default import options.
+   */
+  public function getDefaultImportOptions();
+
+  /****************************************************************************/
+  // Webform field definitions and elements.
+  /****************************************************************************/
+
+  /**
+   * Get a webform's field definitions.
+   *
+   * @return array
+   *   An associative array containing a webform's field definitions.
+   */
+  public function getFieldDefinitions();
+
+  /**
+   * Get webform elements.
+   *
+   * @return array
+   *   An associative array containing webform elements keyed by name.
+   */
+  public function getElements();
+
+  /****************************************************************************/
+  // Export.
+  /****************************************************************************/
+
+  /**
+   * Create CSV export header.
+   *
+   * @return array
+   *   The CSV export header columns.
+   */
+  public function exportHeader();
+
+  /**
+   * Export webform submission as a CSV record.
+   *
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   * @param array $export_options
+   *   An associative array of export options.
+   *
+   * @return array
+   *   The webform submission converted to a CSV record.
+   */
+  public function exportSubmission(WebformSubmissionInterface $webform_submission, array $export_options = []);
+
+  /****************************************************************************/
+  // Import.
+  /****************************************************************************/
+
+  /**
+   * Import records from CSV import file.
+   *
+   * @param int $offset
+   *   Line to be begin importing from.
+   * @param int|null $limit
+   *   The number of records to be imported.
+   *
+   * @return array
+   *   An associate array containing imports states including total,
+   *   created, updated, skipped, and errors.
+   */
+  public function import($offset = 0, $limit = NULL);
+
+  /****************************************************************************/
+  // Summary.
+  /****************************************************************************/
+
+  /**
+   * Total number of submissions to be imported.
+   *
+   * @return int
+   *   The total number of submissions to be imported.
+   */
+  public function getTotal();
+
+  /**
+   * Get source (CSV) columns name.
+   *
+   * @return array
+   *   An associative array containing source (CSV) columns name.
+   */
+  public function getSourceColumns();
+
+  /**
+   * Get destination (field and element) columns name.
+   *
+   * @return array
+   *   An associative array containing destination (field and element)
+   *   columns name.
+   */
+  public function getDestinationColumns();
+
+  /**
+   * Get source (CSV) to destination (field and element) column mapping.
+   *
+   * @return array
+   *   An associative array containing source (CSV) to
+   *   destination (field and element) column mapping.
+   */
+  public function getSourceToDestinationColumnMapping();
+
+  /****************************************************************************/
+  // Batch.
+  /****************************************************************************/
+
+  /**
+   * Get the number of submissions to be exported with each batch.
+   *
+   * @return int
+   *   Number of submissions to be exported with each batch.
+   */
+  public function getBatchLimit();
+
+  /**
+   * Determine if webform submissions must be imported using batch processing.
+   *
+   * @return bool
+   *   TRUE if webform submissions must be imported using batch processing.
+   */
+  public function requiresBatch();
+
+}
diff --git a/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/config/install/webform.webform.test_submission_export_import.yml b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/config/install/webform.webform.test_submission_export_import.yml
new file mode 100644
index 0000000000000000000000000000000000000000..afaa8cf1465533ffa2d4055158de35c35d107c73
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/config/install/webform.webform.test_submission_export_import.yml
@@ -0,0 +1,231 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_submission_export_import_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_submission_export_import
+title: 'Test: Submission: Export/Import'
+description: 'Test webform submission export/import.'
+category: 'Test: Element'
+elements: |
+  summary:
+    '#type': textfield
+    '#title': summary
+  email:
+    '#type': email
+    '#title': email
+  emails:
+    '#type': email
+    '#title': emails
+    '#multiple': true
+  checkbox:
+    '#type': checkbox
+    '#title': checkbox
+  checkboxes:
+    '#type': checkboxes
+    '#title': checkboxes
+    '#options':
+      one: One
+      two: Two
+      three: Three
+  file:
+    '#type': managed_file
+    '#title': file
+  files:
+    '#type': managed_file
+    '#title': files
+    '#multiple': true
+  likert:
+    '#type': webform_likert
+    '#title': likert
+    '#questions':
+      q1: 'Question 1'
+      q2: 'Question 2'
+      q3: 'Question 3'
+    '#answers':
+      1: 'Option 1'
+      2: 'Option 2'
+      3: 'Option 3'
+  composite:
+    '#type': webform_link
+    '#title': composite
+  composites:
+    '#type': webform_link
+    '#title': composites
+    '#multiple': true
+    '#multiple__header': true
+  entity_reference:
+    '#type': webform_entity_select
+    '#title': entity_reference
+    '#target_type': user
+    '#selection_handler': 'default:user'
+    '#selection_settings':
+      include_anonymous: true
+  entity_references:
+    '#type': webform_entity_select
+    '#title': entity_references
+    '#target_type': user
+    '#selection_handler': 'default:user'
+    '#selection_settings':
+      include_anonymous: true
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: both
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/files/test_submission_export_import-external.csv b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/files/test_submission_export_import-external.csv
new file mode 100644
index 0000000000000000000000000000000000000000..7762f76537f642e65526cf45f7c2126ec1189a3d
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/files/test_submission_export_import-external.csv
@@ -0,0 +1,2 @@
+summary,notes,email,emails,checkbox,checkboxes,file,files,likert__q1,likert__q2,likert__q3,composite__title,composite__url,composites,entity_reference,entity_references,not_mapped
+valid external data,valid external data,example@example.com,"example@example.com,random@random.com,test@test.com",1,"two,three,one",https://raw.githubusercontent.com/drupalprojects/webform/8.x-5.x/tests/files/sample.gif,"https://raw.githubusercontent.com/drupalprojects/webform/8.x-5.x/tests/files/sample.gif,https://raw.githubusercontent.com/drupalprojects/webform/8.x-5.x/tests/files/sample.png",3,3,3,Oratione,http://example.com,"[{ title: Oratione, url: 'http://example.com' }, { title: Oratione, url: 'http://example.com' }, { title: Oratione, url: 'http://test.com' }]",user_id,user_uuid,{not mapped}
\ No newline at end of file
diff --git a/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/files/test_submission_export_import-webform.csv b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/files/test_submission_export_import-webform.csv
new file mode 100644
index 0000000000000000000000000000000000000000..3eec3fd52a4b997aea759cdbb62daac9dc18f589
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/files/test_submission_export_import-webform.csv
@@ -0,0 +1,4 @@
+uuid,notes,summary,email,emails,checkbox,checkboxes,file,files,likert__q1,likert__q2,likert__q3,composite__title,composite__url,composites,entity_reference,entity_references,not_mapped
+e1d59c85-7096-4bee-bafa-1bd6798862e2,valid,valid,example@example.com,"example@example.com,random@random.com,test@test.com",1,"two,three,one",https://raw.githubusercontent.com/drupalprojects/webform/8.x-5.x/tests/files/sample.gif,"https://raw.githubusercontent.com/drupalprojects/webform/8.x-5.x/tests/files/sample.gif,https://raw.githubusercontent.com/drupalprojects/webform/8.x-5.x/tests/files/sample.png",3,3,3,Oratione,http://example.com,"[{ title: Oratione, url: 'http://example.com' }, { title: Oratione, url: 'http://example.com' }, { title: Oratione, url: 'http://test.com' }]",user_name,user_mail,{not mapped}
+9a05b67b-a69a-43d8-a498-9bea83c1cbbe,validation warnings,validation warnings,test@test.com,"random@random.com,example@example.com,test@test.com",1,"two,one,three",/webform/plain/tests/files/sample.gif,,3,3,3,Loremipsum,http://test.com,@#$%^not valid ':' yaml,,,{not mapped}
+428e338b-d09c-4bb6-8e34-7dcea79f1f0d,validation errors,validation errors,not an email address,"test@test.com,example@example.com,not an email address",1,invalid,,,1,1,2,Dixisset,http://example.com,"[{ title: Oratione, url: 'http://example.com' }, { title: Oratione, url: 'http://example.com' }, { title: Loremipsum, url: 'http://example.com' }]",,,{not mapped}
\ No newline at end of file
diff --git a/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/js/webform_submission_export_import_test.js b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/js/webform_submission_export_import_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..aac955ab4850cc0ea0133ac3974cc8907663d39a
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/js/webform_submission_export_import_test.js
@@ -0,0 +1,27 @@
+/**
+ * @file
+ * JavaScript behaviors for Webform Export/Import Test module.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * Set import URL and submit the form.
+   *
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.webformSubmissionExportImportTest = {
+    attach: function (context) {
+      $('#edit-import-url--description a', context)
+        .once('webform-export-import-test')
+        .click(function () {
+          $('#edit-import-url').val(this.href);
+          $('#webform-submission-export-import-upload-form').submit();
+          return false;
+        });
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/webform_submission_export_import_test.info.yml b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/webform_submission_export_import_test.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2de933323f34d114be45ba66f8e1e23313efa979
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/webform_submission_export_import_test.info.yml
@@ -0,0 +1,13 @@
+name: 'Webform Submission Export/Import Test'
+type: module
+description: 'Support module for webform submission export/import.'
+package: Testing
+# core: 8.x
+dependencies:
+  - 'webform_image_select:webform_image_select'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/webform_submission_export_import_test.install b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/webform_submission_export_import_test.install
new file mode 100644
index 0000000000000000000000000000000000000000..9c92ee908d92cece4c230a7447922f5042f9fd86
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/webform_submission_export_import_test.install
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Install/uninstall functions for the Webform Export/Import Test module.
+ */
+
+/**
+ * Implements hook_uninstall().
+ */
+function webform_submission_export_import_test_uninstall() {
+  // Delete webform and external CSV import examples.
+  $files_path = drupal_get_path('module', 'webform_submission_export_import_test') . '/files';
+  $file_names = [
+    'public://test_submission_export_import-webform.csv',
+    'public://test_submission_export_import-external.csv',
+  ];
+  foreach ($file_names as $file_name) {
+    if (file_exists("$files_path/$file_name")) {
+      file_unmanaged_delete("$files_path/$file_name");
+    }
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/webform_submission_export_import_test.libraries.yml b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/webform_submission_export_import_test.libraries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..401975897a2d3e9eb29d52ded319df8b769b4a3a
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/webform_submission_export_import_test.libraries.yml
@@ -0,0 +1,8 @@
+webform_submission_export_import_test:
+  version: VERSION
+  js:
+    js/webform_submission_export_import_test.js: {}
+  dependencies:
+    - core/drupal
+    - core/jquery
+    - core/jquery.once
diff --git a/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/webform_submission_export_import_test.module b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/webform_submission_export_import_test.module
new file mode 100644
index 0000000000000000000000000000000000000000..1f030d7bb7edb1d39dad61db79dbb0541bdb16e9
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/tests/modules/webform_submission_export_import_test/webform_submission_export_import_test.module
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @file
+ * Support module for webform submission export/import.
+ */
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\user\Entity\User;
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Make testing the CSV import a little easier by using and prepopulating
+ * external Google Sheet CSV.
+ */
+function webform_submission_export_import_test_form_webform_submission_export_import_upload_form_alter(&$form, FormStateInterface $form_state) {
+  $webform_id = \Drupal::routeMatch()->getRawParameter('webform');
+  if ($webform_id === 'test_submission_export_import' && isset($form['import']['import_url'])) {
+    global $base_url;
+
+    $webform_path = drupal_get_path('module', 'webform');
+    $files_path = drupal_get_path('module', 'webform_submission_export_import_test') . '/files';
+    $file_names = [
+      // @see https://docs.google.com/spreadsheets/d/e/2PACX-1vTYImRfGbGGCtKq7hrYGkm5jJo_P0fwNrLOBpcSapBkEUPlBToShXcSJAdDAF5hjVupbGFAm1anUtip/pub?gid=646036902&single=true&output=csv
+      'webform',
+      // @see https://docs.google.com/spreadsheets/d/e/2PACX-1vTYImRfGbGGCtKq7hrYGkm5jJo_P0fwNrLOBpcSapBkEUPlBToShXcSJAdDAF5hjVupbGFAm1anUtip/pub?gid=173244372&single=true&output=csv
+      'external',
+    ];
+
+    $t_args = [];
+    foreach ($file_names as $file_name) {
+      // Set the test CSV URI.
+      $csv_uri = "public://test_submission_export_import-$file_name.csv";
+
+      // Set file URL href as href.
+      $t_args[":href_$file_name"] = file_create_url($csv_uri);
+
+      // Skip generate test CSV if it exists.
+      if (file_exists($csv_uri)) {
+        continue;
+      }
+
+      // Copy CSV file from module to public:// uri.
+      $csv_file_path = "$files_path/test_submission_export_import-$file_name.csv";
+      file_unmanaged_copy($csv_file_path, NULL, FILE_EXISTS_REPLACE);
+
+      $contents = file_get_contents($csv_uri);
+
+      // Replace GitHub URLs with location URLs.
+      $github_base_url = 'https://raw.githubusercontent.com/drupalprojects/webform/8.x-5.x/';
+      $contents = str_replace($github_base_url, "$base_url/$webform_path/", $contents);
+
+      // Replace user entity reference properties.
+      $account = User::load(1);
+      $contents = str_replace('user_id', $account->id(), $contents);
+      $contents = str_replace('user_name', $account->getAccountName(), $contents);
+      $contents = str_replace('user_mail', $account->getEmail(), $contents);
+      $contents = str_replace('user_uuid', $account->uuid(), $contents);
+
+      file_put_contents($csv_uri, $contents);
+    }
+
+    // Set the default type and URL to the webform.csv.
+    $form['import']['import_type']['#default_value'] = 'url';
+    $form['import']['import_url']['#default_value'] = $t_args[':href_webform'];
+
+    // Display clickable description that populates the import URL and
+    // submits the form.
+    // @see Drupal.behaviors.webformSubmissionExportImportTest
+    $form['import']['import_url']['#description'] = t('Test <a href=":href_webform">webform</a> or <a href=":href_external">external</a>', $t_args);
+    $form['import']['import_url']['#help'] = FALSE;
+    $form['#attached']['library'][] = 'webform_submission_export_import_test/webform_submission_export_import_test';
+  }
+
+  /*
+  // Get webform.csv and external.csv from Google Sheets.
+  file_put_contents(
+     "$files_path/test_submission_export_import-webform.csv",
+    file_get_contents('https://docs.google.com/spreadsheets/d/e/2PACX-1vTYImRfGbGGCtKq7hrYGkm5jJo_P0fwNrLOBpcSapBkEUPlBToShXcSJAdDAF5hjVupbGFAm1anUtip/pub?gid=646036902&single=true&output=csv')
+  );
+  file_put_contents(
+     "$files_path/test_submission_export_import-external.csv",
+    file_get_contents('https://docs.google.com/spreadsheets/d/e/2PACX-1vTYImRfGbGGCtKq7hrYGkm5jJo_P0fwNrLOBpcSapBkEUPlBToShXcSJAdDAF5hjVupbGFAm1anUtip/pub?gid=173244372&single=true&output=csv')
+  );
+  */
+}
diff --git a/web/modules/webform/modules/webform_submission_export_import/tests/src/Functional/WebformSubmissionImportExportFunctionalTest.php b/web/modules/webform/modules/webform_submission_export_import/tests/src/Functional/WebformSubmissionImportExportFunctionalTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..264b721b6b70280dccccadc5e20a7d940c4272a4
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/tests/src/Functional/WebformSubmissionImportExportFunctionalTest.php
@@ -0,0 +1,425 @@
+<?php
+
+namespace Drupal\Tests\webform_submission_import_export\Functional;
+
+use Drupal\file\Entity\File;
+use Drupal\Tests\webform\Functional\WebformBrowserTestBase;
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Entity\WebformSubmission;
+use Drupal\webform\Utility\WebformElementHelper;
+
+/**
+ * Webform submission export/import test.
+ *
+ * @group webform_browser
+ */
+class WebformSubmissionImportExportFunctionalTest extends WebformBrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'file',
+    'webform',
+    'webform_submission_export_import',
+    'webform_submission_export_import_test',
+  ];
+
+  /**
+   * Test submission import.
+   */
+  public function testSubmissionExport() {
+    $this->drupalLogin($this->rootUser);
+
+    $export_csv_uri = 'public://test_submission_export_import-export.csv';
+    $export_csv_url = file_create_url('public://test_submission_export_import-export.csv');
+
+    $webform = Webform::load('test_submission_export_import');
+
+    /** @var \Drupal\webform_submission_export_import\WebformSubmissionExportImportImporterInterface $importer */
+    $importer = \Drupal::service('webform_submission_export_import.importer');
+    $importer->setWebform($webform);
+    $importer->setImportUri($export_csv_url);
+
+    // Create 3 submissions.
+    /** @var \Drupal\webform\WebformSubmissionInterface[] $submissions */
+    $submissions = [
+      WebformSubmission::load($this->postSubmissionTest($webform)),
+      WebformSubmission::load($this->postSubmissionTest($webform)),
+      WebformSubmission::load($this->postSubmissionTest($webform)),
+    ];
+
+    // Create CSV export.
+    $this->drupalPostForm('/admin/structure/webform/manage/test_submission_export_import/results/download', ['exporter' => 'webform_submission_export_import'], t('Download'));
+    file_put_contents($export_csv_uri, $this->getRawContent());
+
+    /**************************************************************************/
+
+    // Import CSV export without any changes.
+    $actual_stats = $importer->import();
+    WebformElementHelper::convertRenderMarkupToStrings($actual_stats);
+    $expected_stats = [
+      'created' => 0,
+      'updated' => 3,
+      'skipped' => 0,
+      'total' => 3,
+      'warnings' => [
+        1 => [],
+        2 => [],
+        3 => [],
+      ],
+      'errors' => [
+        1 => [],
+        2 => [],
+        3 => [],
+      ],
+    ];
+    $this->assertEquals($expected_stats, $actual_stats);
+
+    // Check that submission values are unchanged.
+    foreach ($submissions as $original_submission) {
+      $expected_values = $original_submission->toArray(TRUE);
+      $updated_submission = $this->loadSubmissionByProperty('uuid', $original_submission->uuid());
+      $actual_values = $updated_submission->toArray(TRUE);
+      $this->assertEquals($expected_values, $actual_values);
+    }
+
+    // Alter the first submission.
+    $submissions[0]->setCompletedTime(time() - 1000);
+    $submissions[0]->setNotes('This is a note');
+    $submissions[0]->save();
+
+    // Deleted the third submission.
+    $file_uri = file_create_url(File::load($submissions[2]->getElementData('file'))->getFileUri());
+    $files_uri = file_create_url(File::load($submissions[2]->getElementData('files')[0])->getFileUri());
+    $submissions[2]->delete();
+    unset($submissions[2]);
+
+    // Import CSV export without any changes.
+    $actual_stats = $importer->import();
+    WebformElementHelper::convertRenderMarkupToStrings($actual_stats);
+    $this->debug($actual_stats);
+    $expected_stats = [
+      'created' => 1,
+      'updated' => 2,
+      'skipped' => 0,
+      'total' => 3,
+      'warnings' => [
+        1 => [],
+        2 => [],
+        3 => [
+          0 => '[file] Unable to read file from URL (' . $file_uri . ').',
+          1 => '[files] Unable to read file from URL (' . $files_uri . ').',
+        ],
+      ],
+      'errors' => [
+        1 => [],
+        2 => [],
+        3 => [],
+      ],
+    ];
+    $this->assertEquals($expected_stats, $actual_stats);
+
+    // Check that submission 1 values reset to original values.
+    $original_submission = $submissions[0];
+    $expected_values = $original_submission->toArray(TRUE);
+    $updated_submission = $this->loadSubmissionByProperty('uuid', $original_submission->uuid());
+    $actual_values = $updated_submission->toArray(TRUE);
+
+    // Check that changes and notes were updated.
+    $this->assertNotEqual($expected_values['completed'], $actual_values['completed']);
+    $this->assertNotEqual($expected_values['notes'], $actual_values['notes']);
+
+    // Check that notes was reset.
+    $this->assertEqual('This is a note', $expected_values['notes']);
+    $this->assertEqual('', $actual_values['notes']);
+
+    // Unset changed and notes.
+    unset($expected_values['completed'], $expected_values['notes']);
+    unset($actual_values['completed'], $actual_values['notes']);
+
+    // Check all other values remained the same.
+    $this->assertEquals($expected_values, $actual_values);
+  }
+
+  /**
+   * Test submission import.
+   */
+  public function testSubmissionImport() {
+    $this->drupalLogin($this->rootUser);
+
+    $webform_csv_url = file_create_url('public://test_submission_export_import-webform.csv');
+    $external_csv_url = file_create_url('public://test_submission_export_import-external.csv');
+
+    $webform = Webform::load('test_submission_export_import');
+
+    /** @var \Drupal\webform_submission_export_import\WebformSubmissionExportImportImporterInterface $importer */
+    $importer = \Drupal::service('webform_submission_export_import.importer');
+    $importer->setWebform($webform);
+    $importer->setImportUri($webform_csv_url);
+
+    /**************************************************************************/
+
+    // Upload the webform.csv.
+    $this->drupalPostForm(
+      '/admin/structure/webform/manage/test_submission_export_import/results/upload',
+      ['import_url' => $webform_csv_url],
+      t('Continue')
+    );
+
+    // Check submission count.
+    $this->assertRaw('Are you sure you want to import 3 submissions?');
+
+    // Import only the valid record.
+    $this->drupalPostForm(
+      NULL,
+      ['import_options[treat_warnings_as_errors]' => TRUE, 'confirm' => TRUE],
+      t('Import')
+    );
+
+    // Check import stats.
+    $this->assertRaw('Submission import completed. (total: 3; created: 1; updated: 0; skipped: 2)');
+
+    // Check error messages.
+    $this->assertRaw('<strong>Row #2:</strong> [file] Invalid file URL (/webform/plain/tests/files/sample.gif). URLS must begin with http:// or https://.');
+    $this->assertRaw('<strong>Row #2:</strong> [composites] YAML is not valid. The reserved indicator &quot;@&quot; cannot start a plain scalar; you need to quote the scalar at line 1 (near &quot;@#$%^not valid &#039;:&#039; yaml&quot;).');
+    $this->assertRaw('<strong>Row #3:</strong> The email address <em class="placeholder">not an email address</em> is not valid.');
+    $this->assertRaw('<strong>Row #3:</strong> An illegal choice has been detected. Please contact the site administrator.');
+
+    // Check the submission 1 (valid) record.
+    $submission_1 = $this->loadSubmissionByProperty('notes', 'valid');
+    $this->assertEquals($submission_1->getElementData('summary'), 'valid');
+    $this->assertEquals('e1d59c85-7096-4bee-bafa-1bd6798862e2', $submission_1->uuid());
+    $this->assertEquals($this->rootUser->id(), $submission_1->getOwnerId());
+
+    // Check submission 1 data.
+    $submission_1_expected_data = [
+      'checkbox' => '1',
+      'checkboxes' => [
+        0 => 'two',
+        1 => 'three',
+        2 => 'one',
+      ],
+      'composite' => [
+        'title' => 'Oratione',
+        'url' => 'http://example.com',
+      ],
+      'composites' => [
+        0 => [
+          'title' => 'Oratione',
+          'url' => 'http://example.com',
+        ],
+        1 => [
+          'title' => 'Oratione',
+          'url' => 'http://example.com',
+        ],
+        2 => [
+          'title' => 'Oratione',
+          'url' => 'http://test.com',
+        ],
+      ],
+      'email' => 'example@example.com',
+      'emails' => [
+        0 => 'example@example.com',
+        1 => 'random@random.com',
+        2 => 'test@test.com',
+      ],
+      'entity_reference' => '1',
+      'entity_references' => '1',
+      'file' => '2',
+      'files' => [
+        0 => '3',
+        1 => '4',
+      ],
+      'likert' => [
+        'q1' => '3',
+        'q2' => '3',
+        'q3' => '3',
+      ],
+      'summary' => 'valid',
+    ];
+    $submission_1_actual_data = $submission_1->getData();
+    $this->assertEquals($submission_1_expected_data, $submission_1_actual_data);
+
+    // Re-import the webform.csv using the API with warnings
+    // not treated as errors.
+    $actual_stats = $importer->import();
+    WebformElementHelper::convertRenderMarkupToStrings($actual_stats);
+    $expected_stats = [
+      'created' => 1,
+      'updated' => 1,
+      'skipped' => 1,
+      'total' => 3,
+      'warnings' => [
+        1 => [],
+        2 => [
+          0 => '[file] Invalid file URL (/webform/plain/tests/files/sample.gif). URLS must begin with http:// or https://.',
+          1 => '[composites] YAML is not valid. The reserved indicator &quot;@&quot; cannot start a plain scalar; you need to quote the scalar at line 1 (near &quot;@#$%^not valid &#039;:&#039; yaml&quot;).',
+        ],
+        3 => [],
+      ],
+      'errors' => [
+        1 => [],
+        2 => [],
+        3 => [
+          0 => 'The email address <em class="placeholder">not an email address</em> is not valid.',
+          1 => 'The email address <em class="placeholder">not an email address</em> is not valid.',
+          2 => 'An illegal choice has been detected. Please contact the site administrator.',
+        ],
+      ],
+    ];
+    $this->assertEquals($expected_stats, $actual_stats);
+
+    // Check the submission 2 (validation warnings) record.
+    $submission_2 = $this->loadSubmissionByProperty('notes', 'validation warnings');
+    $this->assertEquals($submission_2->getElementData('summary'), 'validation warnings');
+    $this->assertEquals('9a05b67b-a69a-43d8-a498-9bea83c1cbbe', $submission_2->uuid());
+
+    // Check submission 2 data.
+    $submission_2_actual_data = $submission_2->getData();
+    $submission_2_expected_data = [
+      'checkbox' => '1',
+      'checkboxes' => [
+        0 => 'two',
+        1 => 'one',
+        2 => 'three',
+      ],
+      'composite' => [
+        'title' => 'Loremipsum',
+        'url' => 'http://test.com',
+      ],
+      'email' => 'test@test.com',
+      'emails' => [
+        0 => 'random@random.com',
+        1 => 'example@example.com',
+        2 => 'test@test.com',
+      ],
+      'entity_reference' => '',
+      'entity_references' => '',
+      'file' => '',
+      'likert' => [
+        'q1' => '3',
+        'q2' => '3',
+        'q3' => '3',
+      ],
+      'summary' => 'validation warnings',
+    ];
+    $this->assertEquals($submission_2_expected_data, $submission_2_actual_data);
+
+    // Re-import the webform.csv using the API with warnings
+    // not treated as errors and skipping validation errors.
+    $importer->setImportOptions(['skip_validation' => TRUE]);
+    $actual_stats = $importer->import();
+    WebformElementHelper::convertRenderMarkupToStrings($actual_stats);
+    $expected_stats = [
+      'created' => 1,
+      'updated' => 2,
+      'skipped' => 0,
+      'total' => 3,
+      'warnings' => [
+        1 => [],
+        2 => [
+          0 => '[file] Invalid file URL (/webform/plain/tests/files/sample.gif). URLS must begin with http:// or https://.',
+          1 => '[composites] YAML is not valid. The reserved indicator &quot;@&quot; cannot start a plain scalar; you need to quote the scalar at line 1 (near &quot;@#$%^not valid &#039;:&#039; yaml&quot;).',
+        ],
+        3 => [],
+      ],
+      'errors' => [
+        1 => [],
+        2 => [],
+        3 => [],
+      ],
+    ];
+    $this->assertEquals($expected_stats, $actual_stats);
+
+    // Check the submission 3 (validation warnings) record.
+    $submission_3 = $this->loadSubmissionByProperty('notes', 'validation errors');
+    $this->assertEquals('428e338b-d09c-4bb6-8e34-7dcea79f1f0d', $submission_3->uuid());
+    $this->assertEquals($submission_3->getElementData('summary'), 'validation errors');
+
+    // Check submission 3 contain invalid data.
+    $this->assertEqual($submission_3->getElementData('checkboxes'), ['invalid']);
+    $this->assertEqual($submission_3->getElementData('email'), 'not an email address');
+    $this->assertEqual($submission_3->getElementData('emails')[2], 'not an email address');
+
+    // Set not_mapped destination to summary using the UI.
+    // Upload the webform.csv.
+    $this->drupalPostForm(
+      '/admin/structure/webform/manage/test_submission_export_import/results/upload',
+      ['import_url' => $webform_csv_url],
+      t('Continue')
+    );
+
+    $this->drupalPostForm(
+      NULL,
+      [
+        'import_options[mapping][summary]' => '',
+        'import_options[mapping][not_mapped]' => 'summary',
+        'confirm' => TRUE,
+      ],
+      t('Import')
+    );
+
+    // Check that submission summary now is set to not mapped.
+    $submission_1 = $this->loadSubmissionByProperty('notes', 'valid');
+    $this->assertEquals($submission_1->getElementData('summary'), '{not mapped}');
+
+    // Upload the external.csv.
+    $this->drupalPostForm(
+      '/admin/structure/webform/manage/test_submission_export_import/results/upload',
+      ['import_url' => $external_csv_url],
+      t('Continue')
+    );
+
+    // Check that UUID warning is displayed.
+    $this->assertRaw('No UUID or token was found in the source (CSV). A unique hash will be generated for the each CSV record. Any changes to already an imported record in the source (CSV) will create a new submission.');
+
+    // Import the external.csv.
+    $this->drupalPostForm(NULL, ['confirm' => TRUE], t('Import'));
+
+    // Check that 1 external submission created.
+    $this->assertRaw('Submission import completed. (total: 1; created: 1; updated: 0; skipped: 0)');
+
+    // Check that external submissions exists.
+    $submission_4 = $this->loadSubmissionByProperty('notes', 'valid external data');
+    $this->assertEquals($submission_4->getElementData('summary'), 'valid external data');
+
+    // Upload the external.csv.
+    $this->drupalPostForm(
+      '/admin/structure/webform/manage/test_submission_export_import/results/upload',
+      ['import_url' => $external_csv_url],
+      t('Continue')
+    );
+
+    // Re-import the external.csv.
+    $this->drupalPostForm(NULL, ['confirm' => TRUE], t('Import'));
+
+    // Check that 1 external submission updated.
+    $this->assertRaw('Submission import completed. (total: 1; created: 0; updated: 1; skipped: 0)');
+  }
+
+  /****************************************************************************/
+
+  /**
+   * Load a webform submission using a property value.
+   *
+   * @param string $property
+   *   A submission property.
+   * @param string|int $value
+   *   A property value.
+   *
+   * @return \Drupal\webform\WebformSubmissionInterface
+   *   A webform submission.
+   */
+  protected function loadSubmissionByProperty($property, $value) {
+    /** @var \Drupal\webform\WebformSubmissionStorageInterface $submission_storage */
+    $submission_storage = \Drupal::entityTypeManager()->getStorage('webform_submission');
+
+    // Always reset the cache.
+    $submission_storage->resetCache();
+
+    $submissions = $submission_storage->loadByProperties([$property => $value]);
+    return reset($submissions);
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.info.yml b/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..12660514cb39dafb1a7d576c739adc972b4ee3dc
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.info.yml
@@ -0,0 +1,14 @@
+name: 'Webform Submission Export/Import [EXPERIMENTAL]'
+type: module
+description: 'Provides the ability to export and import submissions.'
+package: Webform
+# core: 8.x
+dependencies:
+  - 'devel:devel'
+  - 'webform:webform'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.links.task.yml b/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.links.task.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ebb608618dd099eeced75ca4f62548e5972b360b
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.links.task.yml
@@ -0,0 +1,15 @@
+entity.webform_submission_export_import.results_import:
+  title: 'Upload'
+  route_name: entity.webform_submission_export_import.results_import
+  parent_id: entity.webform.results
+  weight: 11
+
+# Webform node task.
+# This task will be removed if the webform_node.module is not installed.
+# @see webform_submission_export_import_local_tasks_alter()
+
+entity.node.webform_submission_export_import.results_import:
+  title: 'Upload'
+  route_name: entity.node.webform_submission_export_import.results_import
+  parent_id: entity.node.webform.results
+  weight: 11
diff --git a/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.module b/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.module
new file mode 100644
index 0000000000000000000000000000000000000000..b72e625e6afdc7ed6db2a77017a05310a4f15d2c
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.module
@@ -0,0 +1,254 @@
+<?php
+
+/**
+ * @file
+ * Provides the ability to export and import submissions.
+ */
+
+use Drupal\file\Entity\File;
+
+/**
+ * Implements hook_webform_help_info().
+ */
+function webform_submission_export_import_webform_help_info() {
+  $help['webform_webform_submission_export_import'] = [
+    'group' => 'forms',
+    'title' => t('Upload'),
+    'content' => t('The <strong>Upload</strong> page allows a CSV (comma separated values) file or URL to be uploaded, converted, and imported into webform submissions.'),
+    'video_id' => 'import',
+    'routes' => [
+      // @see /admin/structure/webform/manage/{webform}/results/upload
+      'entity.webform_submission_export_import.results_import',
+      // @see /node/{node}/webform/results/upload
+      'entity.node.webform_submission_export_import.results_import',
+    ],
+  ];
+
+  return $help;
+}
+
+/**
+ * Implements hook_local_tasks_alter().
+ */
+function webform_submission_export_import_local_tasks_alter(&$local_tasks) {
+  // Remove webform node results import if the webform_node.module
+  // is not installed.
+  if (!\Drupal::moduleHandler()->moduleExists('webform_node')) {
+    unset(
+      $local_tasks['entity.node.webform_submission_export_import.results_import']
+    );
+  }
+}
+
+/******************************************************************************/
+// The below code is copy of _file_save_upload_single() which allows imported
+// files to be securely created.
+/******************************************************************************/
+
+/**
+ * Saves a file upload to a new location.
+ *
+ * @param \SplFileInfo $file_info
+ *   The file upload to save.
+ * @param string $form_field_name
+ *   A string that is the associative array key of the upload form element in
+ *   the form array.
+ * @param array $validators
+ *   (optional) An associative array of callback functions used to validate the
+ *   file.
+ * @param bool $destination
+ *   (optional) A string containing the URI that the file should be copied to.
+ * @param int $replace
+ *   (optional) The replace behavior when the destination file already exists.
+ *
+ * @return \Drupal\file\FileInterface|false
+ *   The created file entity or FALSE if the uploaded file not saved.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ *
+ * @internal
+ *   This method should only be called from file_save_upload(). Use that method
+ *   instead.
+ *
+ * @see file_save_upload()
+ * @see file_save_upload_single()
+ */
+function _webform_submission_export_import_file_save_upload_single(\SplFileInfo $file_info, $form_field_name, array $validators = [], $destination = FALSE, $replace = FILE_EXISTS_RENAME) {
+  $user = \Drupal::currentUser();
+  // Check for file upload errors and return FALSE for this file if a lower
+  // level system error occurred. For a complete list of errors:
+  // See http://php.net/manual/features.file-upload.errors.php.
+  switch ($file_info->getError()) {
+    case UPLOAD_ERR_INI_SIZE:
+    case UPLOAD_ERR_FORM_SIZE:
+      \Drupal::messenger()->addError(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', ['%file' => $file_info->getFilename(), '%maxsize' => format_size(file_upload_max_size())]));
+      return FALSE;
+
+    case UPLOAD_ERR_PARTIAL:
+    case UPLOAD_ERR_NO_FILE:
+      \Drupal::messenger()->addError(t('The file %file could not be saved because the upload did not complete.', ['%file' => $file_info->getFilename()]));
+      return FALSE;
+
+    case UPLOAD_ERR_OK:
+      /************************************************************************/
+      // DO NOT USE IF UPLOADED FILE.
+      /************************************************************************/
+      /*
+      // Final check that this is a valid upload, if it isn't, use the
+      // default error handler.
+      if (is_uploaded_file($file_info->getRealPath())) {
+        break;
+      }
+      */
+      break;
+
+    default:
+      // Unknown error
+      \Drupal::messenger()->addError(t('The file %file could not be saved. An unknown error has occurred.', ['%file' => $file_info->getFilename()]));
+      return FALSE;
+
+  }
+
+  // Begin building file entity.
+  $values = [
+    'uid' => $user->id(),
+    'status' => 0,
+    'filename' => $file_info->getClientOriginalName(),
+    'uri' => $file_info->getRealPath(),
+    'filesize' => $file_info->getSize(),
+  ];
+  $values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['filename']);
+  $file = File::create($values);
+
+  $extensions = '';
+  if (isset($validators['file_validate_extensions'])) {
+    if (isset($validators['file_validate_extensions'][0])) {
+      // Build the list of non-munged extensions if the caller provided them.
+      $extensions = $validators['file_validate_extensions'][0];
+    }
+    else {
+      // If 'file_validate_extensions' is set and the list is empty then the
+      // caller wants to allow any extension. In this case we have to remove the
+      // validator or else it will reject all extensions.
+      unset($validators['file_validate_extensions']);
+    }
+  }
+  else {
+    // No validator was provided, so add one using the default list.
+    // Build a default non-munged safe list for file_munge_filename().
+    $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
+    $validators['file_validate_extensions'] = [];
+    $validators['file_validate_extensions'][0] = $extensions;
+  }
+
+  if (!empty($extensions)) {
+    // Munge the filename to protect against possible malicious extension
+    // hiding within an unknown file type (ie: filename.html.foo).
+    $file->setFilename(file_munge_filename($file->getFilename(), $extensions));
+  }
+
+  // Rename potentially executable files, to help prevent exploits (i.e. will
+  // rename filename.php.foo and filename.php to filename.php.foo.txt and
+  // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
+  // evaluates to TRUE.
+  if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
+    $file->setMimeType('text/plain');
+    // The destination filename will also later be used to create the URI.
+    $file->setFilename($file->getFilename() . '.txt');
+    // The .txt extension may not be in the allowed list of extensions. We have
+    // to add it here or else the file upload will fail.
+    if (!empty($extensions)) {
+      $validators['file_validate_extensions'][0] .= ' txt';
+      \Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
+    }
+  }
+
+  // If the destination is not provided, use the temporary directory.
+  if (empty($destination)) {
+    $destination = 'temporary://';
+  }
+
+  // Assert that the destination contains a valid stream.
+  $destination_scheme = file_uri_scheme($destination);
+  if (!file_stream_wrapper_valid_scheme($destination_scheme)) {
+    \Drupal::messenger()->addError(t('The file could not be uploaded because the destination %destination is invalid.', ['%destination' => $destination]));
+    return FALSE;
+  }
+
+  $file->source = $form_field_name;
+  // A file URI may already have a trailing slash or look like "public://".
+  if (substr($destination, -1) != '/') {
+    $destination .= '/';
+  }
+  $file->destination = file_destination($destination . $file->getFilename(), $replace);
+  // If file_destination() returns FALSE then $replace === FILE_EXISTS_ERROR and
+  // there's an existing file so we need to bail.
+  if ($file->destination === FALSE) {
+    \Drupal::messenger()->addError(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', ['%source' => $form_field_name, '%directory' => $destination]));
+    return FALSE;
+  }
+
+  // Add in our check of the file name length.
+  $validators['file_validate_name_length'] = [];
+
+  // Call the validation functions specified by this function's caller.
+  $errors = file_validate($file, $validators);
+
+  // Check for errors.
+  if (!empty($errors)) {
+    $message = [
+      'error' => [
+        '#markup' => t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
+      ],
+      'item_list' => [
+        '#theme' => 'item_list',
+        '#items' => $errors,
+      ],
+    ];
+    // @todo Add support for render arrays in
+    // \Drupal\Core\Messenger\MessengerInterface::addMessage()?
+    // @see https://www.drupal.org/node/2505497.
+    \Drupal::messenger()->addError(\Drupal::service('renderer')->renderPlain($message));
+    return FALSE;
+  }
+
+  $file->setFileUri($file->destination);
+
+  /************************************************************************/
+  // DO NOT USE MOVE UPLOADED FILE.
+  /************************************************************************/
+  // if (!drupal_move_uploaded_file($file_info->getRealPath(), $file->getFileUri())) {
+  if (!file_unmanaged_move($file_info->getRealPath(), $file->getFileUri(), $replace)) {
+    \Drupal::messenger()->addError(t('File upload error. Could not move uploaded file.'));
+    \Drupal::logger('file')->notice('Upload error. Could not move uploaded file %file to destination %destination.', ['%file' => $file->getFilename(), '%destination' => $file->getFileUri()]);
+    return FALSE;
+  }
+
+  // Set the permissions on the new file.
+  drupal_chmod($file->getFileUri());
+
+  // If we are replacing an existing file re-use its database record.
+  // @todo Do not create a new entity in order to update it. See
+  //   https://www.drupal.org/node/2241865.
+  if ($replace == FILE_EXISTS_REPLACE) {
+    $existing_files = entity_load_multiple_by_properties('file', ['uri' => $file->getFileUri()]);
+    if (count($existing_files)) {
+      $existing = reset($existing_files);
+      $file->fid = $existing->id();
+      $file->setOriginalId($existing->id());
+    }
+  }
+
+  // If we made it this far it's safe to record this file in the database.
+  $file->save();
+
+  // Allow an anonymous user who creates a non-public file to see it. See
+  // \Drupal\file\FileAccessControlHandler::checkAccess().
+  if ($user->isAnonymous() && $destination_scheme !== 'public') {
+    $session = \Drupal::request()->getSession();
+    $allowed_temp_files = $session->get('anonymous_allowed_file_ids', []);
+    $allowed_temp_files[$file->id()] = $file->id();
+    $session->set('anonymous_allowed_file_ids', $allowed_temp_files);
+  }
+  return $file;
+}
diff --git a/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.routing.yml b/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..85ecddfb05677dcd670d327d783a4e0097b99faf
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.routing.yml
@@ -0,0 +1,80 @@
+entity.webform_submission_export_import.results_import:
+  path: '/admin/structure/webform/manage/{webform}/results/upload'
+  defaults:
+    _form: '\Drupal\webform_submission_export_import\Form\WebformSubmissionExportImportUploadForm'
+    _title_callback: '\Drupal\webform\Controller\WebformEntityController::title'
+  options:
+    parameters:
+      webform:
+        type: 'entity:webform'
+  requirements:
+    _entity_access: 'webform.submission_update_any'
+    _custom_access: '\Drupal\webform\Access\WebformEntityAccess:checkResultsAccess'
+
+entity.webform_submission_export_import.results_import.example.download:
+  path: '/admin/structure/webform/manage/{webform}/results/upload/example/download'
+  defaults:
+    _controller: '\Drupal\webform_submission_export_import\Controller\WebformSubmissionExportImportController::download'
+  options:
+    parameters:
+      webform:
+        type: 'entity:webform'
+  requirements:
+    _entity_access: 'webform.submission_update_any'
+    _custom_access: '\Drupal\webform\Access\WebformEntityAccess:checkResultsAccess'
+
+entity.webform_submission_export_import.results_import.example.view:
+  path: '/admin/structure/webform/manage/{webform}/results/upload/example/view'
+  defaults:
+    _controller: '\Drupal\webform_submission_export_import\Controller\WebformSubmissionExportImportController::view'
+  options:
+    parameters:
+      webform:
+        type: 'entity:webform'
+  requirements:
+    _entity_access: 'webform.submission_update_any'
+    _custom_access: '\Drupal\webform\Access\WebformEntityAccess:checkResultsAccess'
+
+# Webform node routes.
+# This route will be removed if the webform_node.module is not installed.
+# @see \Drupal\webform_submission_export_import\Routing\WebformSubmissionExportImportRouteSubscriber
+
+entity.node.webform_submission_export_import.results_import:
+  path: '/node/{node}/webform/results/upload'
+  defaults:
+    _form: '\Drupal\webform_submission_export_import\Form\WebformSubmissionExportImportUploadForm'
+    _title_callback: '\Drupal\Core\Entity\Controller\EntityController::title'
+    operation: webform_submission_view
+    entity_access: 'webform.submission_view_any'
+  options:
+    parameters:
+      node:
+        type: 'entity:node'
+  requirements:
+    _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformResultsAccess'
+
+entity.node.webform_submission_export_import.results_import.example.download:
+  path: '/node/{node}/webform/results/upload/example/download'
+  defaults:
+    _controller: '\Drupal\webform_submission_export_import\Controller\WebformSubmissionExportImportController::download'
+    operation: webform_submission_view
+    entity_access: 'webform.submission_view_any'
+  options:
+    parameters:
+      node:
+        type: 'entity:node'
+  requirements:
+    _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformResultsAccess'
+
+entity.node.webform_submission_export_import.results_import.example.view:
+  path: '/node/{node}/webform/results/upload/example/view'
+  defaults:
+    _controller: '\Drupal\webform_submission_export_import\Controller\WebformSubmissionExportImportController::view'
+    operation: webform_submission_view
+    entity_access: 'webform.submission_view_any'
+  options:
+    parameters:
+      node:
+        type: 'entity:node'
+  requirements:
+    _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformResultsAccess'
diff --git a/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.services.yml b/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..db64f978e4f0b6c98cc9db97f1219f985c46b0e8
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_export_import/webform_submission_export_import.services.yml
@@ -0,0 +1,17 @@
+services:
+  webform_submission_export_import.importer:
+    class: Drupal\webform_submission_export_import\WebformSubmissionExportImportImporter
+    arguments: ['@config.factory', '@logger.factory', '@entity_type.manager', '@plugin.manager.webform.element']
+
+  webform_submission_export_import.route_subscriber:
+    class: Drupal\webform_submission_export_import\Routing\WebformSubmissionExportImportRouteSubscriber
+    arguments: ['@module_handler']
+    tags:
+      - { name: event_subscriber }
+
+  # Logger.
+
+  logger.channel.webform_submission_export_import:
+    class: Drupal\Core\Logger\LoggerChannel
+    factory: logger.factory:get
+    arguments: ['webform_submission_export_import']
diff --git a/web/modules/webform/src/Controller/WebformSubmissionLogController.php b/web/modules/webform/modules/webform_submission_log/src/Controller/WebformSubmissionLogController.php
similarity index 72%
rename from web/modules/webform/src/Controller/WebformSubmissionLogController.php
rename to web/modules/webform/modules/webform_submission_log/src/Controller/WebformSubmissionLogController.php
index 460f60b71ec78002fea168a2b53f7867982b152c..dcf251c756a665bd367cde51774a288e348b2336 100644
--- a/web/modules/webform/src/Controller/WebformSubmissionLogController.php
+++ b/web/modules/webform/modules/webform_submission_log/src/Controller/WebformSubmissionLogController.php
@@ -1,14 +1,16 @@
 <?php
 
-namespace Drupal\webform\Controller;
+namespace Drupal\webform_submission_log\Controller;
 
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Datetime\DateFormatterInterface;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformRequestInterface;
 use Drupal\webform\WebformSubmissionInterface;
+use Drupal\webform_submission_log\WebformSubmissionLogManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -42,7 +44,7 @@ class WebformSubmissionLogController extends ControllerBase {
   /**
    * The webform storage.
    *
-   * @var \Drupal\webform\WebformStorageInterface
+   * @var \Drupal\webform\WebformEntityStorageInterface
    */
   protected $webformStorage;
 
@@ -60,6 +62,13 @@ class WebformSubmissionLogController extends ControllerBase {
    */
   protected $requestHandler;
 
+  /**
+   * The webform submission log manager.
+   *
+   * @var \Drupal\webform_submission_log\WebformSubmissionLogManagerInterface
+   */
+  protected $logManager;
+
   /**
    * Constructs a WebformSubmissionLogController object.
    *
@@ -69,14 +78,17 @@ class WebformSubmissionLogController extends ControllerBase {
    *   The date formatter service.
    * @param \Drupal\webform\WebformRequestInterface $request_handler
    *   The webform request handler.
+   * @param \Drupal\webform_submission_log\WebformSubmissionLogManagerInterface $log_manager
+   *   The webform submission log manager.
    */
-  public function __construct(Connection $database, DateFormatterInterface $date_formatter, WebformRequestInterface $request_handler) {
+  public function __construct(Connection $database, DateFormatterInterface $date_formatter, WebformRequestInterface $request_handler, WebformSubmissionLogManagerInterface $log_manager) {
     $this->database = $database;
     $this->dateFormatter = $date_formatter;
     $this->webformStorage = $this->entityTypeManager()->getStorage('webform');
     $this->webformSubmissionStorage = $this->entityTypeManager()->getStorage('webform_submission');
     $this->userStorage = $this->entityTypeManager()->getStorage('user');
     $this->requestHandler = $request_handler;
+    $this->logManager = $log_manager;
   }
 
   /**
@@ -86,7 +98,8 @@ public static function create(ContainerInterface $container) {
     return new static(
       $container->get('database'),
       $container->get('date.formatter'),
-      $container->get('webform.request')
+      $container->get('webform.request'),
+      $container->get('webform_submission_log.manager')
     );
   }
 
@@ -95,78 +108,51 @@ public static function create(ContainerInterface $container) {
    *
    * @param \Drupal\webform\WebformInterface|null $webform
    *   A webform.
-   * @param \Drupal\webform\WebformInterface|null $webform_submission
+   * @param \Drupal\webform\WebformSubmissionInterface|null $webform_submission
    *   A webform submission.
    * @param \Drupal\Core\Entity\EntityInterface|null $source_entity
    *   A source entity.
+   * @param \Drupal\Core\Session\AccountInterface|null $account
+   *   A user account.
    *
    * @return array
    *   A render array as expected by drupal_render().
    */
-  public function overview(WebformInterface $webform = NULL, WebformSubmissionInterface $webform_submission = NULL, EntityInterface $source_entity = NULL) {
+  public function overview(WebformInterface $webform = NULL, WebformSubmissionInterface $webform_submission = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL) {
+    // Entities.
     if (empty($webform) && !empty($webform_submission)) {
       $webform = $webform_submission->getWebform();
     }
     if (empty($source_entity) && !empty($webform_submission)) {
       $source_entity = $webform_submission->getSourceEntity();
     }
+    $webform_entity = $webform_submission ?: $webform;
 
     // Header.
     $header = [];
-    $header['lid'] = ['data' => $this->t('#'), 'field' => 'l.lid', 'sort' => 'desc'];
+    $header['lid'] = ['data' => $this->t('#'), 'field' => 'log.lid', 'sort' => 'desc'];
     if (empty($webform)) {
-      $header['webform_id'] = ['data' => $this->t('Webform'), 'field' => 'l.webform_id', 'class' => [RESPONSIVE_PRIORITY_MEDIUM]];
+      $header['webform_id'] = ['data' => $this->t('Webform'), 'field' => 'log.webform_id', 'class' => [RESPONSIVE_PRIORITY_MEDIUM]];
     }
     if (empty($source_entity) && empty($webform_submission)) {
       $header['entity'] = ['data' => $this->t('Submitted to'), 'class' => [RESPONSIVE_PRIORITY_LOW]];
     }
     if (empty($webform_submission)) {
-      $header['sid'] = ['data' => $this->t('Submission'), 'field' => 'l.sid'];
+      $header['sid'] = ['data' => $this->t('Submission'), 'field' => 'log.sid'];
     }
-    $header['handler_id'] = ['data' => $this->t('Handler'), 'field' => 'l.handler_id'];
-    $header['operation'] = ['data' => $this->t('Operation'), 'field' => 'l.operation', 'class' => [RESPONSIVE_PRIORITY_MEDIUM]];
-    $header['message'] = ['data' => $this->t('Message'), 'field' => 'l.message', 'class' => [RESPONSIVE_PRIORITY_LOW]];
-    $header['uid'] = ['data' => $this->t('User'), 'field' => 'ufd.name', 'class' => [RESPONSIVE_PRIORITY_LOW]];
-    $header['timestamp'] = ['data' => $this->t('Date'), 'field' => 'l.timestamp', 'sort' => 'desc', 'class' => [RESPONSIVE_PRIORITY_LOW]];
+    $header['handler_id'] = ['data' => $this->t('Handler'), 'field' => 'log.handler_id'];
+    $header['operation'] = ['data' => $this->t('Operation'), 'field' => 'log.operation', 'class' => [RESPONSIVE_PRIORITY_MEDIUM]];
+    $header['message'] = ['data' => $this->t('Message'), 'field' => 'log.message', 'class' => [RESPONSIVE_PRIORITY_LOW]];
+    $header['uid'] = ['data' => $this->t('User'), 'field' => 'user.name', 'class' => [RESPONSIVE_PRIORITY_LOW]];
+    $header['timestamp'] = ['data' => $this->t('Date'), 'field' => 'log.timestamp', 'sort' => 'desc', 'class' => [RESPONSIVE_PRIORITY_LOW]];
 
     // Query.
-    $query = $this->database->select('webform_submission_log', 'l')
-      ->extend('\Drupal\Core\Database\Query\PagerSelectExtender')
-      ->extend('\Drupal\Core\Database\Query\TableSortExtender');
-    $query->leftJoin('users_field_data', 'ufd', 'l.uid = ufd.uid');
-    $query->leftJoin('webform_submission', 'ws', 'l.sid = ws.sid');
-    $query->fields('l', [
-      'lid',
-      'uid',
-      'webform_id',
-      'sid',
-      'handler_id',
-      'operation',
-      'message',
-      'timestamp',
-    ]);
-    $query->fields('ws', [
-      'entity_type',
-      'entity_id',
-    ]);
-    if ($webform) {
-      $query->condition('l.webform_id', $webform->id());
-    }
-    if ($webform_submission) {
-      $query->condition('l.sid', $webform_submission->id());
-    }
-    if ($source_entity) {
-      $query->condition('ws.entity_type', $source_entity->getEntityTypeId());
-      $query->condition('ws.entity_id', $source_entity->id());
-    }
-    $result = $query
-      ->limit(50)
-      ->orderByHeader($header)
-      ->execute();
+    $options = ['header' => $header, 'limit' => 50];
+    $logs = $this->logManager->loadByEntities($webform_entity, $source_entity, $account, $options);
 
     // Rows.
     $rows = [];
-    foreach ($result as $log) {
+    foreach ($logs as $log) {
       $row = [];
       $row['lid'] = $log->lid;
       if (empty($webform)) {
@@ -206,7 +192,11 @@ public function overview(WebformInterface $webform = NULL, WebformSubmissionInte
       }
       $row['handler_id'] = $log->handler_id;
       $row['operation'] = $log->operation;
-      $row['message'] = $log->message;
+      $row['message'] = [
+        'data' => [
+          '#markup' => $this->t($log->message, $log->variables),
+        ],
+      ];
       $row['uid'] = [
         'data' => [
           '#theme' => 'username',
@@ -222,10 +212,18 @@ public function overview(WebformInterface $webform = NULL, WebformSubmissionInte
       '#type' => 'table',
       '#header' => $header,
       '#rows' => $rows,
+      '#sticky' => TRUE,
       '#empty' => $this->t('No log messages available.'),
     ];
     $build['pager'] = ['#type' => 'pager'];
     return $build;
   }
 
+  /**
+   * Wrapper that allows the $node to be used as $source_entity.
+   */
+  public function nodeOverview(WebformInterface $webform = NULL, WebformSubmissionInterface $webform_submission = NULL, EntityInterface $node = NULL) {
+    return $this->overview($webform, $webform_submission, $node);
+  }
+
 }
diff --git a/web/modules/webform/modules/webform_submission_log/src/Routing/WebformSubmissionLogRouteSubscriber.php b/web/modules/webform/modules/webform_submission_log/src/Routing/WebformSubmissionLogRouteSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..6f218215c128a7df279dff248e4b44c78334bda5
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_log/src/Routing/WebformSubmissionLogRouteSubscriber.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\webform_submission_log\Routing;
+
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Routing\RouteSubscriberBase;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Remove webform node log routes.
+ */
+class WebformSubmissionLogRouteSubscriber extends RouteSubscriberBase {
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Constructs a WebformSubmissionLogRouteSubscriber object.
+   *
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(ModuleHandlerInterface $module_handler) {
+    $this->moduleHandler = $module_handler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function alterRoutes(RouteCollection $collection) {
+    if (!$this->moduleHandler->moduleExists('webform_node')) {
+      $collection->remove('entity.node.webform.results_log');
+      $collection->remove('entity.node.webform_submission.log');
+    }
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeSubmissionLogTest.php b/web/modules/webform/modules/webform_submission_log/src/Tests/WebformSubmissionLogNodeTest.php
similarity index 69%
rename from web/modules/webform/modules/webform_node/src/Tests/WebformNodeSubmissionLogTest.php
rename to web/modules/webform/modules/webform_submission_log/src/Tests/WebformSubmissionLogNodeTest.php
index 00978dd5a57916cef481066156618d4de8d6aeb0..9ad29003699c539f8066154e433d771fbb892b67 100644
--- a/web/modules/webform/modules/webform_node/src/Tests/WebformNodeSubmissionLogTest.php
+++ b/web/modules/webform/modules/webform_submission_log/src/Tests/WebformSubmissionLogNodeTest.php
@@ -1,20 +1,25 @@
 <?php
 
-namespace Drupal\webform_node\Tests;
+namespace Drupal\webform_submission_log\Tests;
+
+use Drupal\webform\Entity\WebformSubmission;
+use Drupal\webform_node\Tests\WebformNodeTestBase;
 
 /**
  * Tests for webform node submission log.
  *
- * @group WebformNode
+ * @group WebformSubmissionLog
  */
-class WebformNodeSubmissionLogTest extends WebformNodeTestBase {
+class WebformSubmissionLogNodeTest extends WebformNodeTestBase {
+
+  use WebformSubmissionLogTrait;
 
   /**
    * Modules to enable.
    *
    * @var array
    */
-  public static $modules = ['block', 'webform', 'webform_node'];
+  public static $modules = ['block', 'webform', 'webform_node', 'webform_submission_log'];
 
   /**
    * Webforms to load.
@@ -32,13 +37,15 @@ public function testSubmissionLog() {
     $nid = $node->id();
 
     $sid = $this->postNodeSubmission($node);
+    $submission = WebformSubmission::load($sid);
     $log = $this->getLastSubmissionLog();
     $this->assertEqual($log->lid, 1);
     $this->assertEqual($log->sid, 1);
     $this->assertEqual($log->uid, 0);
     $this->assertEqual($log->handler_id, '');
     $this->assertEqual($log->operation, 'submission created');
-    $this->assertEqual($log->message, t('@title: Submission #1 created.', ['@title' => $node->label()]));
+    $this->assertEqual($log->message, '@title created.');
+    $this->assertEqual($log->variables, ['@title' => $submission->label()]);
     $this->assertEqual($log->webform_id, 'test_submission_log');
     $this->assertEqual($log->entity_type, 'node');
     $this->assertEqual($log->entity_id, $node->id());
@@ -51,7 +58,7 @@ public function testSubmissionLog() {
     $this->assertResponse(200);
     $this->assertNoRaw('No log messages available.');
     $this->assertRaw('<a href="' . $base_path . 'node/' . $nid . '/webform/submission/' . $sid . '/log">' . $sid . '</a>');
-    $this->assertRaw(t('@title: Submission #1 created.', ['@title' => $node->label()]));
+    $this->assertRaw(t('@title created.', ['@title' => $submission->label()]));
 
     // Check webform node submission log tab.
     $this->drupalGet("node/$nid/webform/submission/$sid/log");
diff --git a/web/modules/webform/src/Tests/WebformSubmissionLogTest.php b/web/modules/webform/modules/webform_submission_log/src/Tests/WebformSubmissionLogTest.php
similarity index 72%
rename from web/modules/webform/src/Tests/WebformSubmissionLogTest.php
rename to web/modules/webform/modules/webform_submission_log/src/Tests/WebformSubmissionLogTest.php
index c616c288942f1cf65a6f6f96e2884fceb4f4c8a7..e06c5e583b3d2782bf44b77ed76cb46613745b71 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionLogTest.php
+++ b/web/modules/webform/modules/webform_submission_log/src/Tests/WebformSubmissionLogTest.php
@@ -1,33 +1,33 @@
 <?php
 
-namespace Drupal\webform\Tests;
+namespace Drupal\webform_submission_log\Tests;
 
 use Drupal\webform\Entity\Webform;
 use Drupal\webform\Entity\WebformSubmission;
+use Drupal\webform\Tests\WebformTestBase;
 
 /**
- * Tests for webform storage tests.
+ * Tests for webform submission log.
  *
- * @group Webform
+ * @group WebformSubmissionLog
  */
 class WebformSubmissionLogTest extends WebformTestBase {
 
+  use WebformSubmissionLogTrait;
+
   /**
-   * Webforms to load.
+   * Modules to enable.
    *
    * @var array
    */
-  protected static $testWebforms = ['test_submission_log'];
+  public static $modules = ['webform', 'webform_submission_log'];
 
   /**
-   * {@inheritdoc}
+   * Webforms to load.
+   *
+   * @var array
    */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
+  protected static $testWebforms = ['test_submission_log'];
 
   /**
    * Test webform submission log.
@@ -35,8 +35,15 @@ public function setUp() {
   public function testSubmissionLog() {
     global $base_path;
 
+    $admin_user = $this->drupalCreateUser([
+      'administer webform',
+      'access webform submission log',
+    ]);
+
     $webform = Webform::load('test_submission_log');
 
+    /**************************************************************************/
+
     // Check submission created.
     $sid_1 = $this->postSubmission($webform);
     $log = $this->getLastSubmissionLog();
@@ -45,7 +52,8 @@ public function testSubmissionLog() {
     $this->assertEqual($log->uid, 0);
     $this->assertEqual($log->handler_id, '');
     $this->assertEqual($log->operation, 'submission created');
-    $this->assertEqual($log->message, 'Test: Submission: Logging: Submission #1 created.');
+    $this->assertEqual($log->message, '@title created.');
+    $this->assertEqual($log->variables, ['@title' => 'Test: Submission: Logging: Submission #1']);
     $this->assertEqual($log->webform_id, 'test_submission_log');
     $this->assertNull($log->entity_type);
     $this->assertNull($log->entity_id);
@@ -58,7 +66,8 @@ public function testSubmissionLog() {
     $this->assertEqual($log->uid, 0);
     $this->assertEqual($log->handler_id, '');
     $this->assertEqual($log->operation, 'draft created');
-    $this->assertEqual($log->message, 'Test: Submission: Logging: Submission #2 draft created.');
+    $this->assertEqual($log->message, '@title draft created.');
+    $this->assertEqual($log->variables, ['@title' => 'Test: Submission: Logging: Submission #2']);
     $this->assertEqual($log->webform_id, 'test_submission_log');
     $this->assertNull($log->entity_type);
     $this->assertNull($log->entity_id);
@@ -71,7 +80,8 @@ public function testSubmissionLog() {
     $this->assertEqual($log->uid, 0);
     $this->assertEqual($log->handler_id, '');
     $this->assertEqual($log->operation, 'draft updated');
-    $this->assertEqual($log->message, 'Test: Submission: Logging: Submission #2 draft updated.');
+    $this->assertEqual($log->message, '@title draft updated.');
+    $this->assertEqual($log->variables, ['@title' => 'Test: Submission: Logging: Submission #2']);
     $this->assertEqual($log->webform_id, 'test_submission_log');
     $this->assertNull($log->entity_type);
     $this->assertNull($log->entity_id);
@@ -84,38 +94,41 @@ public function testSubmissionLog() {
     $this->assertEqual($log->uid, 0);
     $this->assertEqual($log->handler_id, '');
     $this->assertEqual($log->operation, 'submission completed');
-    $this->assertEqual($log->message, 'Test: Submission: Logging: Submission #2 completed using saved draft.');
+    $this->assertEqual($log->message, '@title completed using saved draft.');
+    $this->assertEqual($log->variables, ['@title' => 'Test: Submission: Logging: Submission #2']);
     $this->assertEqual($log->webform_id, 'test_submission_log');
     $this->assertNull($log->entity_type);
     $this->assertNull($log->entity_id);
 
     // Login admin user.
-    $this->drupalLogin($this->adminWebformUser);
+    $this->drupalLogin($admin_user);
 
     $submission_log = $this->getSubmissionLog();
 
     // Check submission #2 converted.
     $log = $submission_log[0];
     $this->assertEqual($log->lid, 6);
-    $this->assertEqual($log->uid, $this->adminWebformUser->id());
+    $this->assertEqual($log->uid, $admin_user->id());
     $this->assertEqual($log->sid, 2);
     $this->assertEqual($log->operation, 'submission converted');
-    $this->assertEqual($log->message, 'Test: Submission: Logging: Submission #2 converted from anonymous to ' . $this->adminWebformUser->label() . '.');
+    $this->assertEqual($log->message, '@title converted from anonymous to @user.');
+    $this->assertEqual($log->variables, ['@title' => 'Test: Submission: Logging: Submission #2', '@user' => $admin_user->label()]);
 
     // Check submission #1 converted.
     $log = $submission_log[1];
     $this->assertEqual($log->lid, 5);
-    $this->assertEqual($log->uid, $this->adminWebformUser->id());
+    $this->assertEqual($log->uid, $admin_user->id());
     $this->assertEqual($log->sid, 1);
     $this->assertEqual($log->operation, 'submission converted');
-    $this->assertEqual($log->message, 'Test: Submission: Logging: Submission #1 converted from anonymous to ' . $this->adminWebformUser->label() . '.');
+    $this->assertEqual($log->message, '@title converted from anonymous to @user.');
+    $this->assertEqual($log->variables, ['@title' => 'Test: Submission: Logging: Submission #1', '@user' => $admin_user->label()]);
 
     // Check submission updated.
     $this->drupalPostForm("admin/structure/webform/manage/test_submission_log/submission/$sid_2/edit", [], t('Save'));
     $log = $this->getLastSubmissionLog();
     $this->assertEqual($log->lid, 7);
     $this->assertEqual($log->sid, 2);
-    $this->assertEqual($log->uid, $this->adminWebformUser->id());
+    $this->assertEqual($log->uid, $admin_user->id());
     $this->assertEqual($log->handler_id, '');
     /**************************************************************************/
     // $this->assertEqual($log->operation, 'submission completed');
@@ -132,25 +145,25 @@ public function testSubmissionLog() {
     $this->assertFalse($log);
 
     // Check all results log table is empty.
-    $this->drupalGet('admin/structure/webform/submissions/log');
+    $this->drupalGet('/admin/structure/webform/submissions/log');
     $this->assertRaw('No log messages available.');
 
     // Check webform results log table is empty.
-    $this->drupalGet('admin/structure/webform/manage/test_submission_log/results/log');
+    $this->drupalGet('/admin/structure/webform/manage/test_submission_log/results/log');
     $this->assertRaw('No log messages available.');
 
     $sid_3 = $this->postSubmission($webform);
     WebformSubmission::load($sid_3);
 
     // Check all results log table has record.
-    $this->drupalGet('admin/structure/webform/submissions/log');
+    $this->drupalGet('/admin/structure/webform/submissions/log');
     $this->assertNoRaw('No log messages available.');
     $this->assertRaw('<a href="' . $base_path . 'admin/structure/webform/manage/test_submission_log/results/log">Test: Submission: Logging</a>');
     $this->assertRaw('<a href="' . $base_path . 'admin/structure/webform/manage/test_submission_log/submission/3/log">3</a></td>');
     $this->assertRaw('Test: Submission: Logging: Submission #3 created.');
 
     // Check webform results log table has record.
-    $this->drupalGet('admin/structure/webform/manage/test_submission_log/results/log');
+    $this->drupalGet('/admin/structure/webform/manage/test_submission_log/results/log');
     $this->assertNoRaw('No log messages available.');
     $this->assertNoRaw('<a href="' . $base_path . 'admin/structure/webform/manage/test_submission_log/results/log">Test: Submission: Logging</a>');
     $this->assertRaw('<a href="' . $base_path . 'admin/structure/webform/manage/test_submission_log/submission/3/log">3</a></td>');
diff --git a/web/modules/webform/modules/webform_submission_log/src/Tests/WebformSubmissionLogTrait.php b/web/modules/webform/modules/webform_submission_log/src/Tests/WebformSubmissionLogTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..d8678022068f947df7a31c91bfd6b3477b2ba707
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_log/src/Tests/WebformSubmissionLogTrait.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\webform_submission_log\Tests;
+
+/**
+ * Trait for webform submission log tests.
+ */
+trait WebformSubmissionLogTrait {
+
+  /**
+   * Get the last submission id.
+   *
+   * @return int
+   *   The last submission id.
+   */
+  protected function getLastSubmissionLog() {
+    $query = \Drupal::database()->select('webform_submission_log', 'l');
+    $query->leftJoin('webform_submission', 'ws', 'l.sid = ws.sid');
+    $query->fields('l', [
+      'lid',
+      'uid',
+      'sid',
+      'handler_id',
+      'operation',
+      'message',
+      'variables',
+      'timestamp',
+    ]);
+    $query->fields('ws', [
+      'webform_id',
+      'entity_type',
+      'entity_id',
+    ]);
+    $query->orderBy('l.lid', 'DESC');
+    $query->range(0, 1);
+    $submission_log = $query->execute()->fetch();
+    if ($submission_log) {
+      $submission_log->variables = unserialize($submission_log->variables);
+    }
+    return $submission_log;
+  }
+
+  /**
+   * Get the entire submission log.
+   *
+   * @return int
+   *   The last submission id.
+   */
+  protected function getSubmissionLog() {
+    $query = \Drupal::database()->select('webform_submission_log', 'l');
+    $query->leftJoin('webform_submission', 'ws', 'l.sid = ws.sid');
+    $query->fields('l', [
+      'lid',
+      'uid',
+      'sid',
+      'handler_id',
+      'operation',
+      'message',
+      'variables',
+      'timestamp',
+    ]);
+    $query->fields('ws', [
+      'webform_id',
+      'entity_type',
+      'entity_id',
+    ]);
+    $query->orderBy('l.lid', 'DESC');
+    $submission_logs = $query->execute()->fetchAll();
+    foreach ($submission_logs as &$submission_log) {
+      $submission_log->variables = unserialize($submission_log->variables);
+    }
+    return $submission_logs;
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_submission_log/src/WebformSubmissionLogLogger.php b/web/modules/webform/modules/webform_submission_log/src/WebformSubmissionLogLogger.php
new file mode 100644
index 0000000000000000000000000000000000000000..0aa0cd3e85ef8acc144f0668a7436e4051ae5088
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_log/src/WebformSubmissionLogLogger.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\webform_submission_log;
+
+use Drupal\Core\Logger\LogMessageParserInterface;
+use Drupal\Core\Logger\RfcLoggerTrait;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Logger that listens for 'webform_submission' channel.
+ */
+class WebformSubmissionLogLogger implements LoggerInterface {
+
+  use RfcLoggerTrait;
+
+  /**
+   * The webform submission log manager.
+   *
+   * @var \Drupal\webform_submission_log\WebformSubmissionLogManagerInterface
+   */
+  protected $logManager;
+
+  /**
+   * The message's placeholders parser.
+   *
+   * @var \Drupal\Core\Logger\LogMessageParserInterface
+   */
+  protected $parser;
+
+  /**
+   * WebformSubmissionLog constructor.
+   *
+   * @param \Drupal\Core\Logger\LogMessageParserInterface $parser
+   *   The log message parser service.
+   * @param \Drupal\webform_submission_log\WebformSubmissionLogManagerInterface $log_manager
+   *   The webform submission log manager.
+   */
+  public function __construct(LogMessageParserInterface $parser, WebformSubmissionLogManagerInterface $log_manager) {
+    $this->parser = $parser;
+    $this->logManager = $log_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function log($level, $message, array $context = []) {
+    // Only log the 'webform_submission' channel.
+    if ($context['channel'] !== 'webform_submission') {
+      return;
+    }
+
+    // Make sure the context contains a webform submission.
+    if (!isset($context['webform_submission'])) {
+      return;
+    }
+
+    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
+    $webform_submission = $context['webform_submission'];
+
+    // Make sure webform submission log is enabled.
+    if (!$webform_submission->getWebform()->hasSubmissionLog()) {
+      return;
+    }
+
+    // Set default values.
+    $context += [
+      'handler_id' => '',
+      'operation' => '',
+      'data' => [],
+    ];
+
+    // Cast message to string.
+    $message = (string) $message;
+    $message_placeholders = $this->parser->parseMessagePlaceholders($message, $context);
+
+    $this->logManager->insert([
+      'webform_id' => $webform_submission->getWebform()->id(),
+      'sid' => $webform_submission->id(),
+      'handler_id' => $context['handler_id'],
+      'operation' => $context['operation'],
+      'uid' => $context['uid'],
+      'message' => $message,
+      'variables' => serialize($message_placeholders),
+      'data' => serialize($context['data']),
+      'timestamp' => $context['timestamp'],
+    ]);
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_submission_log/src/WebformSubmissionLogManager.php b/web/modules/webform/modules/webform_submission_log/src/WebformSubmissionLogManager.php
new file mode 100644
index 0000000000000000000000000000000000000000..ac6fb420a946cc0ea605174d6fbb0c6fb64dc94c
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_log/src/WebformSubmissionLogManager.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Drupal\webform_submission_log;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformSubmissionInterface;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+
+/**
+ * Webform submission log manager.
+ */
+class WebformSubmissionLogManager implements WebformSubmissionLogManagerInterface {
+  use DependencySerializationTrait;
+
+  /**
+   * Name of the table where log entries are stored.
+   */
+  const TABLE = 'webform_submission_log';
+
+  /**
+   * The database service.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * WebformSubmissionLogManager constructor.
+   *
+   * @param \Drupal\Core\Database\Connection $datababse
+   *   The database service.
+   */
+  public function __construct(Connection $datababse) {
+    $this->database = $datababse;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function insert(array $fields) {
+    $fields += [
+      'webform_id' => '',
+      'sid' => '',
+      'handler_id' => '',
+      'operation' => '',
+      'uid' => '',
+      'message' => '',
+      'variables' => serialize([]),
+      'data' => serialize([]),
+      'timestamp' => '',
+    ];
+    $this->database->insert(self::TABLE)
+      ->fields($fields)
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuery(EntityInterface $webform_entity = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, array $options = []) {
+    // Default options.
+    $options += [
+      'header' => NULL,
+      'limit' => NULL,
+    ];
+
+    $query = $this->database->select(static::TABLE, 'log');
+
+    // Log fields.
+    $query->fields('log', [
+      'lid',
+      'uid',
+      'webform_id',
+      'sid',
+      'handler_id',
+      'operation',
+      'message',
+      'variables',
+      'timestamp',
+      'data',
+    ]);
+
+    // User fields.
+    $query->leftJoin('users_field_data', 'user', 'log.uid = user.uid');
+
+    // Submission fields.
+    $query->leftJoin('webform_submission', 'submission', 'log.sid = submission.sid');
+    $query->fields('submission', [
+      'entity_type',
+      'entity_id',
+    ]);
+
+    // Webform condition.
+    if ($webform_entity instanceof WebformInterface) {
+      $query->condition('log.webform_id', $webform_entity->id());
+    }
+    // Webform submission conditions.
+    elseif ($webform_entity instanceof WebformSubmissionInterface) {
+      $query->condition('log.webform_id', $webform_entity->getWebform()->id());
+      $query->condition('log.sid', $webform_entity->id());
+    }
+
+    // Source entity conditions.
+    if ($source_entity) {
+      $query->condition('submission.entity_type', $source_entity->getEntityTypeId());
+      $query->condition('submission.entity_id', $source_entity->id());
+    }
+
+    // User account condition.
+    if ($account) {
+      $query->condition('log.uid', $account->id());
+    }
+
+    // Set header sorting.
+    if ($options['header']) {
+      $query = $query->extend('\Drupal\Core\Database\Query\TableSortExtender')
+        ->orderByHeader($options['header']);
+    }
+
+    // Set limit pager.
+    if ($options['limit']) {
+      $query = $query->extend('\Drupal\Core\Database\Query\PagerSelectExtender')
+        ->limit($options['limit']);
+    }
+
+    return $query;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadByEntities(EntityInterface $webform_entity = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, array $options = []) {
+    $result = $this->getQuery($webform_entity, $source_entity, $account, $options)
+      ->execute();
+    $records = [];
+    while ($record = $result->fetchObject()) {
+      $record->variables = unserialize($record->variables);
+      $record->data = unserialize($record->data);
+      $records[] = $record;
+    }
+    return $records;
+  }
+
+}
diff --git a/web/modules/webform/modules/webform_submission_log/src/WebformSubmissionLogManagerInterface.php b/web/modules/webform/modules/webform_submission_log/src/WebformSubmissionLogManagerInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..1ed2cd51885d235c16ffe962d4f4dfdd23aee0cb
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_log/src/WebformSubmissionLogManagerInterface.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\webform_submission_log;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Interface for webform submission log manager.
+ */
+interface WebformSubmissionLogManagerInterface {
+
+  /**
+   * Insert submission log.
+   *
+   * @param array $fields
+   *   An associative array of fields to be inserted into the submission log.
+   */
+  public function insert(array $fields);
+
+  /**
+   * Get webform submission log query.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|null $webform_entity
+   *   A webform or webform submission entity.
+   * @param \Drupal\Core\Entity\EntityInterface|null $source_entity
+   *   (optional) A webform submission source entity.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The current user account.
+   * @param array $options
+   *   (optional) Additional options and query conditions.
+   *
+   * @return \Drupal\Core\Database\Query\SelectInterface
+   *   A webform submission log select query.
+   */
+  public function getQuery(EntityInterface $webform_entity = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, array $options = []);
+
+  /**
+   * Log webform submission logs.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|null $webform_entity
+   *   A webform or webform submission entity.
+   * @param \Drupal\Core\Entity\EntityInterface|null $source_entity
+   *   (optional) A webform submission source entity.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The current user account.
+   * @param array $options
+   *   (optional) Additional options and query conditions.
+   *
+   * @return array
+   *   An array of webform submission logs.
+   *   Log entry object includes:
+   *   - lid: (int) ID of the log entry.
+   *   - sid: (int) Webform submission ID on which the operation was executed.
+   *   - uid: (int) UID of the user that executed the operation.
+   *   - handler_id: (string) Optional name of the handler that executed the
+   *     operation.
+   *   - operation: (string) Name of the executed operation.
+   *   - message: (string) Untranslated message of the executed operation.
+   *   - variables: (array) Variables to use whenever message has to be
+   *     translated.
+   *   - data: (array) Data associated with this log entry.
+   *   - timestamp: (int) Timestamp when the operation was executed.
+   */
+  public function loadByEntities(EntityInterface $webform_entity = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, array $options = []);
+
+}
diff --git a/web/modules/webform/modules/webform_submission_log/webform_submission_log.info.yml b/web/modules/webform/modules/webform_submission_log/webform_submission_log.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..400a12eaf9aa8570254a96f3d2e45ae89c9869e9
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_log/webform_submission_log.info.yml
@@ -0,0 +1,13 @@
+name: 'Webform Submission Log'
+type: module
+description: 'Dedicated logging and reporting for webform submissions.'
+package: Webform
+# core: 8.x
+dependencies:
+  - 'webform:webform'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_submission_log/webform_submission_log.install b/web/modules/webform/modules/webform_submission_log/webform_submission_log.install
new file mode 100644
index 0000000000000000000000000000000000000000..7f85f976c592165cc094233cecf2bc4c170ee1c9
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_log/webform_submission_log.install
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * @file
+ * Install, uninstall, and update hooks of the module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function webform_submission_log_schema() {
+  $schema = [];
+
+  $schema['webform_submission_log'] = [
+    'description' => 'Table that contains logs of all webform submission events.',
+    'fields' => [
+      'lid' => [
+        'type' => 'serial',
+        'not null' => TRUE,
+        'description' => 'Primary Key: Unique log event ID.',
+      ],
+      'webform_id' => [
+        'description' => 'The webform id.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ],
+      'sid' => [
+        'description' => 'The webform submission id.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => FALSE,
+      ],
+      'handler_id' => [
+        'description' => 'The webform handler id.',
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => FALSE,
+      ],
+      'uid' => [
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+        'description' => 'The {users}.uid of the user who triggered the event.',
+      ],
+      'operation' => [
+        'type' => 'varchar_ascii',
+        'length' => 64,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Type of operation, for example "save", "sent", or "update."',
+      ],
+      'message' => [
+        'type' => 'text',
+        'not null' => TRUE,
+        'size' => 'big',
+        'description' => 'Text of log message.',
+      ],
+      'variables' => [
+        'type' => 'blob',
+        'not null' => TRUE,
+        'size' => 'big',
+        'description' => 'Serialized array of variables that match the message string and that is passed into the t() function.',
+      ],
+      'data' => [
+        'type' => 'blob',
+        'not null' => TRUE,
+        'size' => 'big',
+        'description' => 'Serialized array of data.',
+      ],
+      'timestamp' => [
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'description' => 'Unix timestamp of when event occurred.',
+      ],
+    ],
+    'primary key' => ['lid'],
+    'indexes' => [
+      'webform_id' => ['webform_id'],
+      'sid' => ['sid'],
+      'uid' => ['uid'],
+      'handler_id' => ['handler_id'],
+      'handler_id_operation' => ['handler_id', 'operation'],
+    ],
+  ];
+
+  return $schema;
+}
diff --git a/web/modules/webform/modules/webform_submission_log/webform_submission_log.links.task.yml b/web/modules/webform/modules/webform_submission_log/webform_submission_log.links.task.yml
new file mode 100644
index 0000000000000000000000000000000000000000..35baeaa824a00f59626edd143ddf449831ae9ef9
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_log/webform_submission_log.links.task.yml
@@ -0,0 +1,33 @@
+entity.webform.results_log:
+  title: 'Log'
+  route_name: entity.webform.results_log
+  parent_id: entity.webform.results
+  weight: 30
+
+entity.webform_submission.collection_log:
+  title: 'Log'
+  route_name: entity.webform_submission.collection_log
+  parent_id: entity.webform_submission.collection
+  weight: 20
+
+entity.webform_submission.log:
+  title: 'Log'
+  route_name: entity.webform_submission.log
+  base_route: entity.webform_submission.canonical
+  weight: 40
+
+# Webform node task.
+# This task will be removed if the webform_node.module is not installed.
+# @see webform_submission_log_local_tasks_alter()
+
+entity.node.webform.results_log:
+  title: 'Log'
+  route_name: entity.node.webform.results_log
+  parent_id: entity.node.webform.results
+  weight: 30
+
+entity.node.webform_submission.log:
+  title: 'Log'
+  route_name: entity.node.webform_submission.log
+  base_route: entity.node.webform_submission.canonical
+  weight: 40
diff --git a/web/modules/webform/modules/webform_submission_log/webform_submission_log.module b/web/modules/webform/modules/webform_submission_log/webform_submission_log.module
new file mode 100644
index 0000000000000000000000000000000000000000..5fef695acadca803158328232165bb9d92abdf9a
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_log/webform_submission_log.module
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @file
+ * Dedicated logging for webform submissions.
+ */
+
+use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformSubmissionInterface;
+
+/**
+ * Implements hook_webform_help_info().
+ */
+function webform_submission_log_webform_help_info() {
+  $help = [];
+  $help['submissions_log'] = [
+    'group' => 'submissions',
+    'title' => t('Submissions: Log'),
+    'content' => t('The <strong>Submissions log</strong> page tracks all submission events for all webforms that have submission logging enabled. Submission logging can be enabled globally or on a per webform basis.'),
+    'routes' => [
+      // @see /admin/structure/webform/results/log
+      'entity.webform_submission.collection_log',
+    ],
+  ];
+  $help['submission_log'] = [
+    'group' => 'submission',
+    'title' => t('Submission: Log'),
+    'content' => t("The <strong>Log</strong> page shows all events and transactions for a submission."),
+    'video_id' => 'submission',
+    'routes' => [
+      // @see /admin/structure/webform/manage/{webform}/submission/{webform_submisssion}/log
+      'entity.webform_submission.log',
+      // @see /node/{node}/webform/submission/{webform_submission}/log
+      'entity.node.webform_submission.log',
+    ],
+  ];
+  $help['results_log'] = [
+    'group' => 'submissions',
+    'title' => t('Results: Log'),
+    'content' => t('The <strong>Results Log</strong> lists all webform submission events for the current webform.'),
+    'routes' => [
+      // @see /admin/structure/webform/manage/{webform}/results/log
+      'entity.webform.results_log',
+    ],
+  ];
+  $help['webform_node_results_log'] = [
+    'group' => 'webform_nodes',
+    'title' => t('Webform Node: Results: Log'),
+    'content' => t('The <strong>Results Log</strong> lists all webform submission events for the current webform.'),
+    'routes' => [
+      // @see /node/{node}/webform/results/log
+      'entity.node.webform.results_log',
+    ],
+  ];
+  return $help;
+}
+
+/**
+ * Implements hook_local_tasks_alter().
+ */
+function webform_submission_log_local_tasks_alter(&$local_tasks) {
+  // Remove webform node log if the webform_node.module is not installed.
+  if (!\Drupal::moduleHandler()->moduleExists('webform_node')) {
+    unset(
+      $local_tasks['entity.node.webform.results_log'],
+      $local_tasks['entity.node.webform_submission.log']
+    );
+  }
+}
+
+/**
+ * Implements hook_webform_delete().
+ */
+function webform_submission_log_webform_delete(WebformInterface $webform) {
+  \Drupal::database()->delete('webform_submission_log')
+    ->condition('webform_id', $webform->id())
+    ->execute();
+}
+
+/**
+ * Implements hook_webform_submission_delete().
+ */
+function webform_submission_log_webform_submission_delete(WebformSubmissionInterface $webform_submission) {
+  \Drupal::database()->delete('webform_submission_log')
+    ->condition('sid', $webform_submission->id())
+    ->execute();
+}
diff --git a/web/modules/webform/modules/webform_submission_log/webform_submission_log.permissions.yml b/web/modules/webform/modules/webform_submission_log/webform_submission_log.permissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b17ce68d7e203158e1af524acad0e240f9068611
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_log/webform_submission_log.permissions.yml
@@ -0,0 +1,3 @@
+'access webform submission log':
+  title: 'Access the webform submission log'
+  description: 'Allows viewing of <em>all</em> submission events, if the user can access a webform''s results.'
diff --git a/web/modules/webform/modules/webform_submission_log/webform_submission_log.routing.yml b/web/modules/webform/modules/webform_submission_log/webform_submission_log.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d5204fef56e2c137a979bb45cabd7621cc0f2b46
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_log/webform_submission_log.routing.yml
@@ -0,0 +1,68 @@
+entity.webform_submission.collection_log:
+  path: '/admin/structure/webform/submissions/log'
+  defaults:
+    _controller: '\Drupal\webform_submission_log\Controller\WebformSubmissionLogController::overview'
+    _title: 'Webforms: Submissions log'
+  requirements:
+    _permission: 'access webform submission log'
+    _custom_access: '\Drupal\webform\Access\WebformAccountAccess:checkSubmissionAccess'
+
+entity.webform.results_log:
+  path: '/admin/structure/webform/manage/{webform}/results/log'
+  defaults:
+    _controller: '\Drupal\webform_submission_log\Controller\WebformSubmissionLogController::overview'
+    _title_callback: '\Drupal\webform\Controller\WebformEntityController::title'
+  options:
+    parameters:
+      webform:
+        type: 'entity:webform'
+  requirements:
+    _permission: 'access webform submission log'
+    _entity_access: 'webform.submission_view_any'
+    _custom_access: '\Drupal\webform\Access\WebformEntityAccess:checkLogAccess'
+
+entity.webform_submission.log:
+  path: '/admin/structure/webform/manage/{webform}/submission/{webform_submission}/log'
+  defaults:
+    _controller: '\Drupal\webform_submission_log\Controller\WebformSubmissionLogController::overview'
+    _title_callback: '\Drupal\webform\Controller\WebformSubmissionViewController::title'
+  requirements:
+    _permission: 'access webform submission log'
+    _entity_access: 'webform_submission.view_any'
+    _custom_access: '\Drupal\webform\Access\WebformEntityAccess::checkLogAccess'
+
+# Webform node routes.
+# This routes will be removed if the webform_node.module is not installed.
+# @see \Drupal\webform_submission_log\Routing\WebformSubmissionLogRouteSubscriber
+
+entity.node.webform.results_log:
+  path: '/node/{node}/webform/results/log'
+  defaults:
+    _controller: '\Drupal\webform_submission_log\Controller\WebformSubmissionLogController::nodeOverview'
+    _title_callback: '\Drupal\Core\Entity\Controller\EntityController::title'
+    operation: webform_submission_view
+    entity_access: 'webform.submission_view_any'
+  options:
+    _admin_route: TRUE
+    parameters:
+      node:
+        type: 'entity:node'
+  requirements:
+    _permission: 'access webform submission log'
+    _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformLogAccess'
+
+entity.node.webform_submission.log:
+  path: '/node/{node}/webform/submission/{webform_submission}/log'
+  defaults:
+    _controller: '\Drupal\webform_submission_log\Controller\WebformSubmissionLogController::overview'
+    _title_callback: '\Drupal\webform\Controller\WebformSubmissionViewController::title'
+    operation: webform_submission_view
+    entity_access: 'webform.submission_view_any'
+  options:
+    _admin_route: TRUE
+    parameters:
+      node:
+        type: 'entity:node'
+  requirements:
+    _permission: 'access webform submission log'
+    _custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformLogAccess'
diff --git a/web/modules/webform/modules/webform_submission_log/webform_submission_log.services.yml b/web/modules/webform/modules/webform_submission_log/webform_submission_log.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fdd1f39565358cfc0f23ae48170168f56edaec75
--- /dev/null
+++ b/web/modules/webform/modules/webform_submission_log/webform_submission_log.services.yml
@@ -0,0 +1,16 @@
+services:
+  webform_submission_log.manager:
+    class: Drupal\webform_submission_log\WebformSubmissionLogManager
+    arguments: ['@database']
+
+  logger.webform_submission_log:
+    class: Drupal\webform_submission_log\WebformSubmissionLogLogger
+    arguments: ['@logger.log_message_parser', '@webform_submission_log.manager']
+    tags:
+      - { name: logger }
+
+  webform_submission_log.route_subscriber:
+    class: Drupal\webform_submission_log\Routing\WebformSubmissionLogRouteSubscriber
+    arguments: ['@module_handler']
+    tags:
+      - { name: event_subscriber }
diff --git a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_contact.yml b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_contact.yml
index 2468be32568fd7dd5345badf142c93a67ce48286..29ad230d81772311194e4550b6cedbed209d274d 100644
--- a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_contact.yml
+++ b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_contact.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_templates
 open: null
 close: null
+weight: 0
 uid: null
 template: true
+archive: false
 id: template_contact
 title: 'Contact Us'
 description: 'A basic contact webform template.'
@@ -41,6 +43,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -48,6 +51,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -63,22 +67,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -89,6 +108,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -107,9 +127,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -162,6 +184,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_confirmation:
     id: email
@@ -179,17 +205,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: '[webform_submission:values:subject:raw]'
       body: '[webform_submission:values:message:value]'
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
@@ -205,7 +233,7 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
@@ -219,9 +247,11 @@ handlers:
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_employee_evaluation.yml b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_employee_evaluation.yml
index 084f19a3b0eabaee2f23aedc02f62a78c2ceebf0..077908e1b943af82bb3c085ae5f890b4250295db 100644
--- a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_employee_evaluation.yml
+++ b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_employee_evaluation.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_templates
 open: null
 close: null
+weight: 0
 uid: null
 template: true
+archive: false
 id: template_employee_evaluation
 title: 'Employee Evaluation'
 description: 'An employee evaluation webform template.'
@@ -15,7 +17,7 @@ category: ''
 elements: |
   evaluator_information:
     '#title': 'Your Information'
-    '#type': fieldset
+    '#type': webform_section
     evaluator_first_name:
       '#title': 'First Name'
       '#type': textfield
@@ -37,7 +39,7 @@ elements: |
       '#required': true
   employee_information:
     '#title': 'Employee Information'
-    '#type': fieldset
+    '#type': webform_section
     employee_first_name:
       '#title': 'First Name'
       '#type': textfield
@@ -52,7 +54,7 @@ elements: |
       '#required': true
     employee_ratings:
       '#type': webform_likert
-      '#title': 'How would you rate the employee''s...'
+      '#title': 'How would you rate the employee''s…'
       '#questions':
         attendance: Attendance
         attire: Attire
@@ -88,6 +90,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -95,6 +98,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -110,22 +114,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -136,6 +155,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -154,9 +174,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -209,4 +231,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_feedback.yml b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_feedback.yml
index 8b8a13c402e8c432e8c37c5f593d698600c03d3a..0f650f548bc47a9aea7749cc6b069a44120cbcd2 100644
--- a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_feedback.yml
+++ b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_feedback.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_templates
 open: null
 close: null
+weight: 0
 uid: null
 template: true
+archive: false
 id: template_feedback
 title: Feedback
 description: 'A basic feedback template.'
@@ -46,6 +48,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -53,6 +56,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -68,22 +72,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -94,6 +113,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -112,9 +132,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -167,6 +189,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_confirmation:
     id: email
@@ -184,17 +210,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
+      from_name: _default
+      subject: _default
       body: '[webform_submission:values:comments:value]'
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
@@ -220,7 +248,7 @@ handlers:
           text: '[site:mail]'
         - value: _other_
           text: '[site:mail]'
-        - value: _default_
+        - value: _default
           text: '[site:mail]'
       cc_mail: ''
       cc_options: {  }
@@ -229,14 +257,16 @@ handlers:
       from_mail: '[webform_submission:values:email:raw]'
       from_options: {  }
       from_name: '[webform_submission:values:name:raw]'
-      subject: default
+      subject: _default
       body: '[webform_submission:values:comments:value]'
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_issue.yml b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_issue.yml
index 1e83fb0cf6c786e5d5cac81edb0dffbd2a8f88d5..107689908a809022edada9c0cad565eb61e71cf6 100644
--- a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_issue.yml
+++ b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_issue.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_templates
 open: null
 close: null
+weight: 0
 uid: null
 template: true
+archive: false
 id: template_issue
 title: Issue
 description: 'An issue webform template.'
@@ -129,6 +131,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -136,6 +139,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -151,22 +155,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: false
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -177,6 +196,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -195,9 +215,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -250,4 +272,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_job_application.yml b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_job_application.yml
index cf7236b20c39055f33f22efcb9c5b43ba0ac14fb..65fc513c3e57e440fdaf1a5e9cab574996a8d14c 100644
--- a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_job_application.yml
+++ b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_job_application.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_templates
 open: null
 close: null
+weight: 0
 uid: null
 template: true
+archive: false
 id: template_job_application
 title: 'Job Application'
 description: 'A job application webform template.'
@@ -15,7 +17,7 @@ category: ''
 elements: |
   information:
     '#title': 'Your Information'
-    '#type': fieldset
+    '#type': webform_section
     first_name:
       '#title': 'First Name'
       '#type': textfield
@@ -25,13 +27,13 @@ elements: |
       '#type': textfield
       '#required': true
     gender:
-      '#type': radios
+      '#type': webform_radios_other
       '#title': Gender
       '#options': gender
       '#required': true
   contact_information:
     '#title': 'Contact Information'
-    '#type': fieldset
+    '#type': webform_section
     contact:
       '#type': webform_contact
       '#title': Contact
@@ -40,7 +42,7 @@ elements: |
       '#company__access': false
   resume:
     '#title': 'Your Resume'
-    '#type': fieldset
+    '#type': webform_section
     resume_method:
       '#type': radios
       '#options':
@@ -89,6 +91,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -96,6 +99,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -111,22 +115,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -137,6 +156,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -155,9 +175,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -210,6 +232,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_notification:
     id: email
@@ -221,7 +247,7 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
@@ -230,14 +256,16 @@ handlers:
       from_mail: '[webform_submission:values:email:raw]'
       from_options: {  }
       from_name: '[webform_submission:values:first_name] [webform_submission:values:last_name]'
-      subject: default
-      body: default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_job_seeker_profile.yml b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_job_seeker_profile.yml
index 29581be22b8ad0341c2e618e15bee8ced0e69df6..419cd3700a0869b5187311f4fb0b17db400cee43 100644
--- a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_job_seeker_profile.yml
+++ b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_job_seeker_profile.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_templates
 open: null
 close: null
+weight: 0
 uid: null
 template: true
+archive: false
 id: template_job_seeker_profile
 title: 'Job Seeker Profile'
 description: 'A job seeker profile webform template.'
@@ -21,9 +23,10 @@ elements: |
       <li>Providing more information gives a better picture to employers</li>
       <li>Salary requirements, location preferences and skill level are all important factors in the hiring decision</li>
       </ul>
+      
   information:
     '#title': 'Job Seeker Information'
-    '#type': fieldset
+    '#type': webform_section
     first_name:
       '#title': 'First Name'
       '#type': textfield
@@ -88,6 +91,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -95,6 +99,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -110,22 +115,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -136,6 +156,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -154,9 +175,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -209,6 +232,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_notification:
     id: email
@@ -220,7 +247,7 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
@@ -229,14 +256,16 @@ handlers:
       from_mail: '[webform_submission:values:email:raw]'
       from_options: {  }
       from_name: '[webform_submission:values:first_name] [webform_submission:values:last_name]'
-      subject: default
-      body: default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_registration.yml b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_registration.yml
index 4d9f8cbc5a130bed52220660af1ee0ea09fa6172..1bcb4171240ca9f6ea811960875ebd06e0e70005 100644
--- a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_registration.yml
+++ b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_registration.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_templates
 open: null
 close: null
+weight: 0
 uid: null
 template: true
+archive: false
 id: template_registration
 title: Registration
 description: 'A registration webform template.'
@@ -15,7 +17,7 @@ category: ''
 elements: |
   personal_information:
     '#title': 'Your Personal Information'
-    '#type': fieldset
+    '#type': webform_section
     first_name:
       '#title': 'First Name'
       '#type': textfield
@@ -26,7 +28,7 @@ elements: |
       '#required': true
   contact_information:
     '#title': 'Your Contact Information'
-    '#type': fieldset
+    '#type': webform_section
     contact:
       '#type': webform_contact
       '#title': Contact
@@ -35,13 +37,13 @@ elements: |
       '#company__access': false
   mailinglist:
     '#title': 'Mailing List'
-    '#type': fieldset
+    '#type': webform_section
     subscribe:
       '#title': 'Please subscribe me to your mailing list.'
       '#type': checkbox
   additional_information:
     '#title': 'Additional Information'
-    '#type': fieldset
+    '#type': webform_section
     '#open': true
     notes:
       '#title': Comments
@@ -58,6 +60,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -65,6 +68,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -80,22 +84,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -106,6 +125,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -124,9 +144,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -179,6 +201,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_notification:
     id: email
@@ -190,7 +216,7 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
@@ -199,14 +225,16 @@ handlers:
       from_mail: '[webform_submission:values:email:raw]'
       from_options: {  }
       from_name: '[webform_submission:values:first_name] [webform_submission:values:last_name]'
-      subject: default
-      body: default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_session_evaluation.yml b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_session_evaluation.yml
index bbfef35fc38c421fdad14fde1c2489dad644cfae..7ae8180a0874eb816419373c951b42681bb2496c 100644
--- a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_session_evaluation.yml
+++ b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_session_evaluation.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_templates
 open: null
 close: null
+weight: 0
 uid: null
 template: true
+archive: false
 id: template_session_evaluation
 title: 'Session Evaluation'
 description: 'A session evaluation webform template.'
@@ -21,7 +23,7 @@ elements: |
     '#required': true
   speaker:
     '#type': webform_likert
-    '#title': 'Please rate the speaker''s...'
+    '#title': 'Please rate the speaker''s…'
     '#questions':
       mastery: 'Mastery of this topic'
       presentation: 'Presentation skills'
@@ -50,6 +52,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -57,6 +60,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -72,22 +76,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: false
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -98,6 +117,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -116,9 +136,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -171,6 +193,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_notification:
     id: email
@@ -182,7 +208,7 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
@@ -191,14 +217,16 @@ handlers:
       from_mail: '[webform_submission:values:email:raw]'
       from_options: {  }
       from_name: '[webform_submission:values:first_name] [webform_submission:values:last_name]'
-      subject: default
-      body: default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_subscribe.yml b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_subscribe.yml
index 703d98c0ae7ea514da5e1efcba9d249e562a9515..774eb2f32daaf253bc0e34b52683931c5cfa82ac 100644
--- a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_subscribe.yml
+++ b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_subscribe.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_templates
 open: null
 close: null
+weight: 0
 uid: null
 template: true
+archive: false
 id: template_subscribe
 title: Subscribe
 description: 'A subscribe to mailing list webform template.'
@@ -37,6 +39,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -44,6 +47,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -59,22 +63,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -85,6 +104,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -103,9 +123,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -158,6 +180,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_notification:
     id: email
@@ -169,7 +195,7 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
@@ -178,14 +204,16 @@ handlers:
       from_mail: '[webform_submission:values:email:raw]'
       from_options: {  }
       from_name: '[webform_submission:values:first_name] [webform_submission:values:last_name]'
-      subject: default
-      body: default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_user_profile.yml b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_user_profile.yml
index 7d86ab92e5c07e93a06737ea072a3d71f4fb8dc6..7102fd34f1c80d942d989bad76736be21495cb8b 100644
--- a/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_user_profile.yml
+++ b/web/modules/webform/modules/webform_templates/config/install/webform.webform.template_user_profile.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_templates
 open: null
 close: null
+weight: 0
 uid: null
 template: true
+archive: false
 id: template_user_profile
 title: 'User Profile'
 description: 'A user profile webform template.'
@@ -15,7 +17,7 @@ category: ''
 elements: |
   account_information:
     '#title': 'Your Account Information'
-    '#type': fieldset
+    '#type': webform_section
     user_name:
       '#type': textfield
       '#title': 'User Name'
@@ -27,7 +29,7 @@ elements: |
       '#file_extensions': 'gif jpg png svg'
   personal_information:
     '#title': 'Your Personal Information'
-    '#type': fieldset
+    '#type': webform_section
     first_name:
       '#title': 'First Name'
       '#type': textfield
@@ -41,7 +43,7 @@ elements: |
       '#title': Country
       '#options': country_names
     location:
-      '#type': webform_location
+      '#type': webform_location_places
       '#title': Location
       '#title_display': invisible
       '#description': 'Your location will be saved and may be shared.'
@@ -55,7 +57,7 @@ elements: |
       '#select2': true
       '#options': languages
     gender:
-      '#type': radios
+      '#type': webform_radios_other
       '#title': Gender
       '#options': gender
     biography:
@@ -79,7 +81,7 @@ elements: |
       '#description': 'Your GitHub user name.'
   work_information:
     '#title': 'Your Work Information'
-    '#type': fieldset
+    '#type': webform_section
     current_organization:
       '#type': textfield
       '#title': 'Current Organization'
@@ -98,7 +100,7 @@ elements: |
       '#select2': true
   email_settings:
     '#title': 'Email addresses'
-    '#type': fieldset
+    '#type': webform_section
     email:
       '#type': email
       '#title': 'Primary E-mall Address'
@@ -109,7 +111,7 @@ elements: |
       '#description': 'Enter multiple email addresses separated by commas.'
   regional_settings:
     '#title': 'Regional Settings'
-    '#type': fieldset
+    '#type': webform_section
     time_zone:
       '#type': select
       '#title': Timezone
@@ -131,6 +133,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -138,6 +141,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -153,22 +157,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -179,6 +198,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -197,9 +217,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -252,6 +274,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_notification:
     id: email
@@ -263,7 +289,7 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
@@ -272,14 +298,16 @@ handlers:
       from_mail: '[webform_submission:values:email:raw]'
       from_options: {  }
       from_name: '[webform_submission:values:first_name] [webform_submission:values:last_name]'
-      subject: default
-      body: default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/modules/webform_templates/src/Controller/WebformTemplatesController.php b/web/modules/webform/modules/webform_templates/src/Controller/WebformTemplatesController.php
index 7ae7cc6f1d36dfa93f5ffecb9857febcab5cf588..700ee7d5404804128bc10757997ffed3eb5ae643 100644
--- a/web/modules/webform/modules/webform_templates/src/Controller/WebformTemplatesController.php
+++ b/web/modules/webform/modules/webform_templates/src/Controller/WebformTemplatesController.php
@@ -49,7 +49,7 @@ class WebformTemplatesController extends ControllerBase implements ContainerInje
    * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
    *   The webform builder.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
-   *   The entity manager.
+   *   The entity type manager.
    */
   public function __construct(AccountInterface $current_user, FormBuilderInterface $form_builder, EntityTypeManagerInterface $entity_type_manager) {
     $this->currentUser = $current_user;
@@ -73,12 +73,14 @@ public static function create(ContainerInterface $container) {
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
    *   The current request.
+   * @param bool $manage
+   *   Manage templates.
    *
    * @return array|RedirectResponse
    *   A render array representing the webform templates index page or redirect
    *   response to a selected webform via the filter's autocomplete.
    */
-  public function index(Request $request) {
+  public function index(Request $request, $manage = FALSE) {
     $keys = $request->get('search');
     $category = $request->get('category');
 
@@ -89,12 +91,14 @@ public function index(Request $request) {
       }
     }
 
-    $header = [
-      $this->t('Title'),
-      ['data' => $this->t('Description'), 'class' => [RESPONSIVE_PRIORITY_LOW]],
-      ['data' => $this->t('Category'), 'class' => [RESPONSIVE_PRIORITY_LOW]],
-      ['data' => $this->t('Operations'), 'colspan' => 2],
-    ];
+    $header = [];
+    $header['title'] = $this->t('Title');
+    $header['description'] = ['data' => $this->t('Description'), 'class' => [RESPONSIVE_PRIORITY_LOW]];
+    $header['category'] = ['data' => $this->t('Category'), 'class' => [RESPONSIVE_PRIORITY_LOW]];
+    if ($manage) {
+      $header['owner'] = ['data' => $this->t('Author'), 'class' => [RESPONSIVE_PRIORITY_LOW]];
+    }
+    $header['operations'] = ['data' => $this->t('Operations')];
 
     $webforms = $this->getTemplates($keys, $category);
     $rows = [];
@@ -104,20 +108,65 @@ public function index(Request $request) {
       $row['title'] = $webform->toLink();
       $row['description']['data']['#markup'] = $webform->get('description');
       $row['category']['data']['#markup'] = $webform->get('category');
-      if ($this->currentUser->hasPermission('create webform')) {
+
+      if ($manage) {
+        $row['owner'] = ($owner = $webform->getOwner()) ? $owner->toLink() : '';
+
+        $operations = [];
+        if ($webform->access('update')) {
+          $operations['edit'] = [
+            'title' => $this->t('Build'),
+            'url' => $this->ensureDestination($webform->toUrl('edit-form')),
+          ];
+        }
+        if ($webform->access('submission_page')) {
+          $operations['view'] = [
+            'title' => $this->t('View'),
+            'url' => $webform->toUrl('canonical'),
+          ];
+        }
+        if ($webform->access('update')) {
+          $operations['settings'] = [
+            'title' => $this->t('Settings'),
+            'url' => $webform->toUrl('settings'),
+          ];
+        }
+        if ($webform->access('duplicate')) {
+          $operations['duplicate'] = [
+            'title' => $this->t('Duplicate'),
+            'url' => $webform->toUrl('duplicate-form', ['query' => ['template' => 1]]),
+            'attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW),
+          ];
+        }
+        if ($webform->access('delete') && $webform->hasLinkTemplate('delete-form')) {
+          $operations['delete'] = [
+            'title' => $this->t('Delete'),
+            'url' => $this->ensureDestination($webform->toUrl('delete-form')),
+            'attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW),
+          ];
+        }
+        $row['operations']['data'] = [
+          '#type' => 'operations',
+          '#links' => $operations,
+          '#prefix' => '<div class="webform-dropbutton">',
+          '#suffix' => '</div>',
+        ];
+      }
+      else {
         $row['operations']['data']['select'] = [
           '#type' => 'link',
           '#title' => $this->t('Select'),
           '#url' => Url::fromRoute('entity.webform.duplicate_form', $route_parameters),
           '#attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW, ['button', 'button--primary']),
         ];
+        $row['operations']['data']['preview'] = [
+          '#type' => 'link',
+          '#title' => $this->t('Preview'),
+          '#url' => Url::fromRoute('entity.webform.preview', $route_parameters),
+          '#attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NORMAL, ['button']),
+        ];
       }
-      $row['operations']['data']['preview'] = [
-        '#type' => 'link',
-        '#title' => $this->t('Preview'),
-        '#url' => Url::fromRoute('entity.webform.preview', $route_parameters),
-        '#attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NORMAL, ['button']),
-      ];
+
       $rows[] = $row;
     }
 
@@ -137,6 +186,7 @@ public function index(Request $request) {
       '#type' => 'table',
       '#header' => $header,
       '#rows' => $rows,
+      '#sticky' => TRUE,
       '#empty' => $this->t('There are no templates available.'),
       '#cache' => [
         'contexts' => $this->webformStorage->getEntityType()->getListCacheContexts(),
@@ -183,6 +233,7 @@ public function previewForm(Request $request, WebformInterface $webform) {
   protected function getTemplates($keys = '', $category = '') {
     $query = $this->webformStorage->getQuery();
     $query->condition('template', TRUE);
+    $query->condition('archive', FALSE);
     // Filter by key(word).
     if ($keys) {
       $or = $query->orConditionGroup()
@@ -245,4 +296,17 @@ protected function isAdmin() {
     return ($this->currentUser->hasPermission('administer webform') || $this->currentUser->hasPermission('edit any webform'));
   }
 
+  /**
+   * Ensures that a destination is present on the given URL.
+   *
+   * @param \Drupal\Core\Url $url
+   *   The URL object to which the destination should be added.
+   *
+   * @return \Drupal\Core\Url
+   *   The updated URL object.
+   */
+  protected function ensureDestination(Url $url) {
+    return $url->mergeOptions(['query' => $this->getRedirectDestination()->getAsArray()]);
+  }
+
 }
diff --git a/web/modules/webform/modules/webform_templates/src/Form/WebformTemplatesFilterForm.php b/web/modules/webform/modules/webform_templates/src/Form/WebformTemplatesFilterForm.php
index a05279a7156b1d17da18f069d04e478445f5d1c2..40f130fc3a99085c8fd250ed6e7f4e806e8987be 100644
--- a/web/modules/webform/modules/webform_templates/src/Form/WebformTemplatesFilterForm.php
+++ b/web/modules/webform/modules/webform_templates/src/Form/WebformTemplatesFilterForm.php
@@ -10,7 +10,7 @@
 /**
  * Provides the webform templates filter webform.
  */
-class WebformtemplatesFilterForm extends FormBase {
+class WebformTemplatesFilterForm extends FormBase {
 
   /**
    * {@inheritdoc}
diff --git a/web/modules/webform/modules/webform_templates/src/Tests/WebformTemplatesTest.php b/web/modules/webform/modules/webform_templates/src/Tests/WebformTemplatesTest.php
index 26c164b3c375fcced68e69b28f2f454ce3796b01..98021690168d9972987ecebf1f67f3f040bfb810 100644
--- a/web/modules/webform/modules/webform_templates/src/Tests/WebformTemplatesTest.php
+++ b/web/modules/webform/modules/webform_templates/src/Tests/WebformTemplatesTest.php
@@ -31,8 +31,19 @@ class WebformTemplatesTest extends WebformTestBase {
    * Tests webform templates.
    */
   public function testTemplates() {
-    // Login the own user.
-    $this->drupalLogin($this->rootUser);
+    $user_account = $this->drupalCreateUser([
+      'access webform overview',
+      'administer webform',
+    ]);
+
+    $admin_account = $this->drupalCreateUser([
+      'access webform overview',
+      'administer webform',
+      'administer webform templates',
+    ]);
+
+    // Login the user.
+    $this->drupalLogin($user_account);
 
     $template_webform = Webform::load('test_form_template');
 
@@ -42,14 +53,33 @@ public function testTemplates() {
     $this->assertTrue($template_webform->isClosed());
 
     // Check template is included in the 'Templates' list display.
-    $this->drupalGet('admin/structure/webform/templates');
+    $this->drupalGet('/admin/structure/webform/templates');
     $this->assertRaw('Test: Webform: Template');
     $this->assertRaw('Test using a webform as a template.');
 
     // Check template is accessible to user with create webform access.
-    $this->drupalGet('webform/test_form_template');
+    $this->drupalGet('/webform/test_form_template');
     $this->assertResponse(200);
     $this->assertRaw('You are previewing the below template,');
+
+    // Check select template clears the description.
+    $this->drupalGet('/admin/structure/webform/manage/test_form_template/duplicate');
+    $this->assertFieldByName('description[value]', '');
+
+    // Check that admin can not access manage templates.
+    $this->drupalGet('/admin/structure/webform/templates/manage');
+    $this->assertResponse(403);
+
+    // Login the admin.
+    $this->drupalLogin($admin_account);
+
+    // Check that admin can access manage templates.
+    $this->drupalGet('/admin/structure/webform/templates/manage');
+    $this->assertResponse(200);
+
+    // Check select template clears the description.
+    $this->drupalGet('/admin/structure/webform/manage/test_form_template/duplicate', ['query' => ['template' => 1]]);
+    $this->assertFieldByName('description[value]', 'Test using a webform as a template.');
   }
 
 }
diff --git a/web/modules/webform/modules/webform_templates/webform_templates.info.yml b/web/modules/webform/modules/webform_templates/webform_templates.info.yml
index f4ca970e988eeb9e77d77ebe64a4a6e4ee122df2..e4e725cc5a0ae7c063cf1b2d2ec61b845813e4f0 100644
--- a/web/modules/webform/modules/webform_templates/webform_templates.info.yml
+++ b/web/modules/webform/modules/webform_templates/webform_templates.info.yml
@@ -6,8 +6,8 @@ package: Webform
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_templates/webform_templates.links.task.yml b/web/modules/webform/modules/webform_templates/webform_templates.links.task.yml
index 0bf39c9af94c1bd9b4ffb125b88df5634c05e6e9..c3f5645cbe6cb144ae75519120e74f96331ce4b0 100644
--- a/web/modules/webform/modules/webform_templates/webform_templates.links.task.yml
+++ b/web/modules/webform/modules/webform_templates/webform_templates.links.task.yml
@@ -3,3 +3,15 @@ entity.webform.templates:
   route_name: entity.webform.templates
   base_route: entity.webform.collection
   weight: 10
+
+entity.webform.templates.view:
+  title: 'List'
+  route_name: entity.webform.templates
+  parent_id: entity.webform.templates
+  weight: 0
+
+entity.webform.templates.manage:
+  title: 'Manage'
+  route_name: entity.webform.templates.manage
+  parent_id: entity.webform.templates
+  weight: 10
diff --git a/web/modules/webform/modules/webform_templates/webform_templates.permissions.yml b/web/modules/webform/modules/webform_templates/webform_templates.permissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5e871e765e77bdefede732c69746949fb9decada
--- /dev/null
+++ b/web/modules/webform/modules/webform_templates/webform_templates.permissions.yml
@@ -0,0 +1,4 @@
+'administer webform templates':
+  title: 'Administer webform templates'
+  description: 'Allows administration to manage webform templates.'
+  restrict access: true
diff --git a/web/modules/webform/modules/webform_templates/webform_templates.routing.yml b/web/modules/webform/modules/webform_templates/webform_templates.routing.yml
index a26f7ba9e985be6a42888184077f303ea105d5d0..9e0fb7f25d8d1d08344602d21698c6cba0d9a253 100644
--- a/web/modules/webform/modules/webform_templates/webform_templates.routing.yml
+++ b/web/modules/webform/modules/webform_templates/webform_templates.routing.yml
@@ -4,7 +4,17 @@ entity.webform.templates:
     _controller: '\Drupal\webform_templates\Controller\WebformTemplatesController::index'
     _title: 'Webforms: Templates'
   requirements:
-    _custom_access: '\Drupal\webform\Access\WebformAccountAccess::checkOverviewAccess'
+    _custom_access: '\Drupal\webform\Access\WebformAccountAccess::checkTemplatesAccess'
+
+entity.webform.templates.manage:
+  path: '/admin/structure/webform/templates/manage'
+  defaults:
+    _controller: '\Drupal\webform_templates\Controller\WebformTemplatesController::index'
+    _title: 'Webforms: Templates'
+    manage: true
+  requirements:
+    _custom_access: '\Drupal\webform\Access\WebformAccountAccess::checkTemplatesAccess'
+    _permission: 'administer webform templates'
 
 entity.webform.preview:
   path: '/webform/{webform}/preview'
@@ -20,4 +30,4 @@ entity.webform.templates.autocomplete:
     _controller: '\Drupal\webform\Controller\WebformEntityController::autocomplete'
     templates: TRUE
   requirements:
-    _custom_access: '\Drupal\webform\Access\WebformAccountAccess::checkOverviewAccess'
+    _custom_access: '\Drupal\webform\Access\WebformAccountAccess::checkTemplatesAccess'
diff --git a/web/modules/webform/modules/webform_ui/css/webform_ui.module.css b/web/modules/webform/modules/webform_ui/css/webform_ui.module.css
index 26a468856b59391dd5136eda7504849b2ba7bc08..9d2561c864f97bd059a2fe388d9dd8ed62f5abdf 100644
--- a/web/modules/webform/modules/webform_ui/css/webform_ui.module.css
+++ b/web/modules/webform/modules/webform_ui/css/webform_ui.module.css
@@ -14,6 +14,25 @@ thead th .dropbutton {
   text-transform: none;
 }
 
+/**
+ * Actions.
+ *
+ * Secondary buttons when .button--primary is removed.
+ * @see Drupal.behaviors.webformUiElementsActionsSecondary
+ */
+.action-links .button--secondary.button--primary {
+  display: none;
+}
+
+/**
+ * Hide secondary buttons for mobile.
+ */
+@media screen and (max-width: 600px) {
+  .action-links .button--secondary {
+    display: none;
+  }
+}
+
 /**
  * Elements (/admin/structure/webform/manage/{webform})
  */
@@ -63,6 +82,13 @@ thead th .dropbutton {
   font-weight: bold;
 }
 
+.webform-ui-elements-table tr.webform-ui-element-type-details input,
+.webform-ui-elements-table tr.webform-ui-element-type-details select,
+.webform-ui-elements-table tr.webform-ui-element-type-fieldset input,
+.webform-ui-elements-table tr.webform-ui-element-type-fieldset select {
+  font-weight: normal;
+}
+
 .webform-ui-elements-table tr.webform-ui-element-type-webform_actions td {
   border-top: 1px solid #a6a6a6;
   border-bottom: 1px solid #a6a6a6;
@@ -70,6 +96,24 @@ thead th .dropbutton {
   font-weight: bold;
 }
 
+.webform-ui-elements-table tr.webform-ui-element-type-webform_actions input,
+.webform-ui-elements-table tr.webform-ui-element-type-webform_actions select {
+  font-weight: normal;
+}
+
+@media screen and (max-width: 1280px) {
+  .js-off-canvas-dialog-open .webform-ui-elements-table th,
+  .js-off-canvas-dialog-open .webform-ui-elements-table td {
+    display: none;
+  }
+  .js-off-canvas-dialog-open .webform-ui-elements-table th:first-child,
+  .js-off-canvas-dialog-open .webform-ui-elements-table th:last-child,
+  .js-off-canvas-dialog-open .webform-ui-elements-table td:first-child,
+  .js-off-canvas-dialog-open .webform-ui-elements-table td:last-child {
+    display: table-cell;
+  }
+}
+
 /**
  * Element types (/admin/structure/webform/manage/[webform_id]/element/add)
  */
@@ -131,7 +175,7 @@ thead th .dropbutton {
 }
 
 /**
- * Element (/admin/structure/webform/manage/[webform_id]/element/add/[elment_type])
+ * Element (/admin/structure/webform/manage/[webform_id]/element/add/[element_type])
  * @see \Drupal\webform\Plugin\WebformElementBase::form
  */
 .form--inline.webform-ui-element-form-inline--input .form-item {
diff --git a/web/modules/webform/modules/webform_ui/js/webform_ui.js b/web/modules/webform/modules/webform_ui/js/webform_ui.js
index 6f4d96c59654a4371dde7ca9e094311f9cfe6e6e..467019721b2967a0844369d9cfd3ad4a85a53a2c 100644
--- a/web/modules/webform/modules/webform_ui/js/webform_ui.js
+++ b/web/modules/webform/modules/webform_ui/js/webform_ui.js
@@ -8,22 +8,104 @@
   'use strict';
 
   /**
-   * Lock the default actions element by moving it to the table footer (<tfoot>).
+   * Remove .button-primary class from .action-links .button-secondary.
+   *
+   * The seven.theme adds the .button-primary class to all actions.
    *
    * @type {Drupal~behavior}
    *
-   * @prop {Drupal~behaviorAttach} attach
-   *   Attaches the behavior for locking the default actions element by moving
-   *   it to the table footer (<tfoot>).
+   * @see webform_ui_preprocess_menu_local_action()
+   * @see seven_preprocess_menu_local_action()
+   * @see webform_ui.module.css
    */
-  Drupal.behaviors.webformUiElementsActionsDefault = {
+  Drupal.behaviors.webformUiElementsActionsSecondary = {
     attach: function (context, settings) {
-      $(context).find('[data-drupal-selector="edit-webform-ui-elements-webform-actions-default"]').once('webform-ui-elements-webform-actions-default').each(function () {
-        var $tr = $(this);
-        var $table = $tr.parents('table');
-        $table.append($('<tfoot></tfoot>').append($tr));
+      $(context).find('.action-links .button--secondary').once('webform-ui-elements-webform-actions-secondary').each(function () {
+        $(this).removeClass('button--primary');
       });
     }
   };
 
+  /**
+   * Adds keyboard support to the form builder.
+   *
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.webformUiElementsKeyboard = {
+    attach: function (context, settings) {
+      var $table = $(context)
+        .find('.webform-ui-elements-table')
+        .once('webform-ui-elements-keyboard');
+
+      // Disable autosubmit when Enter is pressed on 'Required' checkboxes.
+      $table.find('td input:checkbox')
+        .on('keyup keypress', function (e) {
+          if (e.which === 13) {
+            e.preventDefault();
+            return false;
+          }
+        });
+
+      // Move keyboard focus up (38) or down (40).
+      $table.find('td:first-child a:not(.tabledrag-handle), td input:checkbox, td .webform-dropbutton li.dropbutton-action a, td .webform-dropbutton button')
+        .on('keydown', function (event) {
+          if (event.which === 38 || event.which === 40) {
+            var $cell = $(this).closest('td');
+            var $row = $cell.parent();
+            var direction = (event.which === 38) ? 'prev' : 'next';
+            var index = $cell.index();
+            var tagName = this.tagName;
+            while ($row[direction]().length) {
+              $row = $row[direction]();
+              $cell = $row.find('td').eq(index).find(tagName);
+              if ($cell.length) {
+                $cell.focus();
+                break;
+              }
+            }
+            event.preventDefault();
+          }
+        });
+
+      // Move keyboard focus left (37) or right (39).
+      $table.find('td a:not(.tabledrag-handle), td input, td select, td button')
+        .on('keydown', function (event) {
+          if (event.which === 37 || event.which === 39) {
+            var $cell = $(this).closest('td');
+            var direction = (event.which === 37) ? 'prev' : 'next';
+            var $focus;
+
+
+            // Move keyboard focus within operations dropbutton.
+            if ($(this).closest('.webform-dropbutton').length) {
+              if (direction === 'next' &&
+                this.tagName === 'A' &&
+                $(this).parent('.dropbutton-action').length) {
+                $cell.find('button').focus();
+                event.preventDefault();
+                return;
+              }
+              else if (direction === 'prev' && this.tagName === 'BUTTON') {
+                $cell.find('a').focus();
+                event.preventDefault();
+                return;
+              }
+            }
+
+            while ($cell.length) {
+              $cell = $cell[direction]();
+              $focus = $cell.find('a:visible, input:visible, select:visible');
+              if ($focus.length) {
+                $focus.focus();
+                event.preventDefault();
+                return;
+              }
+            }
+          }
+
+        });
+
+    }
+  };
+
 })(jQuery, Drupal, drupalSettings);
diff --git a/web/modules/webform/modules/webform_ui/src/Access/WebformUiAccess.php b/web/modules/webform/modules/webform_ui/src/Access/WebformUiAccess.php
index d4d774d86c70cb7398d9d275c42fe9588a69caee..3bdf4a1583cda9f0baa86365353174f12419ed50 100644
--- a/web/modules/webform/modules/webform_ui/src/Access/WebformUiAccess.php
+++ b/web/modules/webform/modules/webform_ui/src/Access/WebformUiAccess.php
@@ -24,7 +24,8 @@ class WebformUiAccess {
    *   The access result.
    */
   public static function checkWebformSourceAccess(WebformInterface $webform, AccountInterface $account) {
-    return AccessResult::allowedIf($webform->access('update', $account) && $account->hasPermission('edit webform source'));
+    return $webform->access('update', $account, TRUE)
+      ->andIf(AccessResult::allowedIfHasPermission($account, 'edit webform source'));
   }
 
   /**
@@ -39,7 +40,8 @@ public static function checkWebformSourceAccess(WebformInterface $webform, Accou
    *   The access result.
    */
   public static function checkWebformOptionSourceAccess(WebformOptionsInterface $webform_options, AccountInterface $account) {
-    return AccessResult::allowedIf($webform_options->access('update', $account) && $account->hasPermission('edit webform source'));
+    return $webform_options->access('update', $account, TRUE)
+      ->andIf(AccessResult::allowedIfHasPermission($account, 'edit webform source'));
   }
 
   /**
@@ -54,7 +56,31 @@ public static function checkWebformOptionSourceAccess(WebformOptionsInterface $w
    *   The access result.
    */
   public static function checkWebformEditAccess(WebformInterface $webform, AccountInterface $account) {
-    return AccessResult::allowedIf($webform->access('update', $account));
+    return $webform->access('update', $account, TRUE);
+  }
+
+  /**
+   * Check that webform element type can be added by a user.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   * @param string $type
+   *   An element type.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   Run access checks for this account.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public static function checkWebformElementAccess(WebformInterface $webform, $type, AccountInterface $account) {
+    /** @var \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager */
+    $element_manager = \Drupal::service('plugin.manager.webform.element');
+
+    $access = $webform->access('update', $account, TRUE);
+    $access = $access->andIf(!$element_manager->isExcluded($type) ? AccessResult::allowed() : AccessResult::forbidden());
+    $access->addCacheableDependency($webform);
+    $access->addCacheableDependency(\Drupal::config('webform.settings'));
+    return $access;
   }
 
 }
diff --git a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementAddForm.php b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementAddForm.php
index f950b67371af9e70ef1062fc76ae18a9780e40b5..9ed5534b4bfe0074fbec981ca312b5ee647b0f3e 100644
--- a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementAddForm.php
+++ b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementAddForm.php
@@ -11,6 +11,11 @@
  */
 class WebformUiElementAddForm extends WebformUiElementFormBase {
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $operation = 'create';
+
   /**
    * {@inheritdoc}
    */
@@ -21,7 +26,7 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
 
     $parent_key = $this->getRequest()->get('parent');
     if ($parent_key) {
-      $parent_element = $webform->getElementDecoded($parent_key);
+      $parent_element = $webform->getElement($parent_key);
       if (!$parent_element) {
         throw new NotFoundHttpException();
       }
@@ -29,7 +34,14 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
 
     $element_plugin = $this->getWebformElementPlugin();
 
-    $form['#title'] = $this->t('Add @label element', ['@label' => $element_plugin->getPluginLabel()]);
+    $t_args = ['@label' => $element_plugin->getPluginLabel()];
+    if ($parent_key) {
+      $t_args['@parent'] = $parent_element['#admin_title'] ?: $parent_element['#title'] ?: $parent_element['#webform_key'];
+      $form['#title'] = $this->t('Add @label element to "@parent"', $t_args);
+    }
+    else {
+      $form['#title'] = $this->t('Add @label element', $t_args);
+    }
 
     $form = parent::buildForm($form, $form_state, $webform, NULL, $parent_key);
     if (isset($form['properties']['element']['title'])) {
diff --git a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementDeleteForm.php b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementDeleteForm.php
index 6e18e7ba76d060038dd8bb3de9eaa379de22c94a..5f154253b168851b4fc57b278352fbb679dfc51d 100644
--- a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementDeleteForm.php
+++ b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementDeleteForm.php
@@ -3,10 +3,9 @@
 namespace Drupal\webform_ui\Form;
 
 use Drupal\Component\Render\FormattableMarkup;
-use Drupal\Core\Form\ConfirmFormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\RendererInterface;
-use Drupal\webform\Form\WebformDialogFormTrait;
+use Drupal\webform\Form\WebformDeleteFormBase;
 use Drupal\webform\Plugin\WebformElementManagerInterface;
 use Drupal\webform\WebformEntityElementsValidatorInterface;
 use Drupal\webform\WebformInterface;
@@ -16,9 +15,7 @@
 /**
  * Webform for deleting a webform element.
  */
-class WebformUiElementDeleteForm extends ConfirmFormBase {
-
-  use WebformDialogFormTrait;
+class WebformUiElementDeleteForm extends WebformDeleteFormBase {
 
   /**
    * The renderer.
@@ -96,84 +93,86 @@ public static function create(ContainerInterface $container) {
     );
   }
 
+  /****************************************************************************/
+  // Delete form.
+  /****************************************************************************/
+
   /**
    * {@inheritdoc}
    */
-  public function getDescription() {
-    $t_args = [
-      '%element' => $this->getElementTitle(),
-      '%webform' => $this->webform->label(),
-    ];
-
-    $build = [];
-    $element_plugin = $this->getWebformElementPlugin();
-    if ($element_plugin->isContainer($this->element)) {
-      $build['warning'] = [
-        '#markup' => $this->t('This will immediately delete the %element container and all nested elements within %element from the %webform webform. This cannot be undone.', $t_args),
+  public function getQuestion() {
+    if ($this->isDialog()) {
+      $t_args = [
+        '@title' => $this->getElementTitle(),
       ];
+      return $this->t("Delete the '@title' element?", $t_args);
     }
     else {
-      $build['warning'] = [
-        '#markup' => $this->t('This will immediately delete the %element element from the %webform webform. This cannot be undone.', $t_args),
+      $t_args = [
+        '%webform' => $this->webform->label(),
+        '%title' => $this->getElementTitle(),
       ];
+      return $this->t('Delete the %title element from the %webform webform?', $t_args);
     }
+  }
 
-    if ($this->element['#webform_children']) {
-      $build['elements'] = $this->getDeletedElementsItemList($this->element['#webform_children']);
-      $build['elements']['#title'] = t('The below nested elements will be also deleted.');
-    }
-
-    return $this->renderer->renderPlain($build);
+  /**
+   * {@inheritdoc}
+   */
+  public function getWarning() {
+    $t_args = ['%title' => $this->getElementTitle()];
+    return [
+      '#type' => 'webform_message',
+      '#message_type' => 'warning',
+      '#message_message' => $this->t('Are you sure you want to delete the %title element?', $t_args) . '<br/>' .
+        '<strong>' . $this->t('This action cannot be undone.') . '</strong>',
+    ];
   }
 
   /**
-   * Get deleted elements as item list.
-   *
-   * @param array $children
-   *   An array child key.
-   *
-   * @return array
-   *   A render array representing an item list of elements.
+   * {@inheritdoc}
    */
-  protected function getDeletedElementsItemList(array $children) {
-    if (empty($children)) {
-      return [];
-    }
+  public function getDescription() {
+    $element_plugin = $this->getWebformElementPlugin();
 
     $items = [];
-    foreach ($children as $key) {
-      $element = $this->webform->getElement($key);
-      if (isset($element['#title'])) {
-        $title = new FormattableMarkup('@title (@key)', ['@title' => $element['#title'], '@key' => $key]);
-      }
-      else {
-        $title = $key;
-      }
-      $items[$key]['title'] = ['#markup' => $title];
-      if ($element['#webform_children']) {
-        $items[$key]['items'] = $this->getDeletedElementsItemList($element['#webform_children']);
-      }
+    $items[] = $this->t('Remove this element');
+    $items[] = $this->t('Delete any submission data associated with this element');
+    if ($element_plugin->isContainer($this->element)) {
+      $items[] = $this->t('Delete all child elements');
     }
 
     return [
-      '#theme' => 'item_list',
-      '#items' => $items,
+      'title' => [
+        '#markup' => $this->t('This action will…'),
+      ],
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => $items,
+      ],
     ];
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getQuestion() {
-    return $this->t('Are you sure you want to delete the %title element from the %webform webform?', ['%webform' => $this->webform->label(), '%title' => $this->getElementTitle()]);
+  public function getDetails() {
+    $elements = $this->getDeletedElementsItemList($this->element['#webform_children']);
+    if ($elements) {
+      return [
+        '#type' => 'details',
+        '#title' => $this->t('Nested elements being deleted'),
+        'elements' => $elements,
+      ];
+    }
+    else {
+      return [];
+    }
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getConfirmText() {
-    return $this->t('Delete');
-  }
+  /****************************************************************************/
+  // Form methods.
+  /****************************************************************************/
 
   /**
    * {@inheritdoc}
@@ -213,10 +212,49 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     $this->webform->deleteElement($this->key);
     $this->webform->save();
 
-    drupal_set_message($this->t('The webform element %title has been deleted.', ['%title' => $this->getElementTitle()]));
+    $this->messenger()->addStatus($this->t('The webform element %title has been deleted.', ['%title' => $this->getElementTitle()]));
     $form_state->setRedirectUrl($this->webform->toUrl('edit-form'));
   }
 
+  /****************************************************************************/
+  // Helper methods.
+  /****************************************************************************/
+
+  /**
+   * Get deleted elements as item list.
+   *
+   * @param array $children
+   *   An array child key.
+   *
+   * @return array
+   *   A render array representing an item list of elements.
+   */
+  protected function getDeletedElementsItemList(array $children) {
+    if (empty($children)) {
+      return [];
+    }
+
+    $items = [];
+    foreach ($children as $key) {
+      $element = $this->webform->getElement($key);
+      if (isset($element['#title'])) {
+        $title = new FormattableMarkup('@title (@key)', ['@title' => $element['#title'], '@key' => $key]);
+      }
+      else {
+        $title = $key;
+      }
+      $items[$key]['title'] = ['#markup' => $title];
+      if ($element['#webform_children']) {
+        $items[$key]['items'] = $this->getDeletedElementsItemList($element['#webform_children']);
+      }
+    }
+
+    return [
+      '#theme' => 'item_list',
+      '#items' => $items,
+    ];
+  }
+
   /**
    * Get the webform element's title or key.
    *
diff --git a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementDuplicateForm.php b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementDuplicateForm.php
index ca007fc18dcc43f682311f87d69c072d4ed4dc16..c806efbf9159abc89f32cc59912cb243e2b8bc0b 100644
--- a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementDuplicateForm.php
+++ b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementDuplicateForm.php
@@ -11,6 +11,11 @@
  */
 class WebformUiElementDuplicateForm extends WebformUiElementFormBase {
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $operation = 'duplicate';
+
   /**
    * {@inheritdoc}
    */
@@ -26,9 +31,8 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
 
     $element_initialized = $webform->getElement($key);
 
-    $form['#title'] = $this->t('Duplicate @title element', [
-      '@title' => (!empty($this->element['#title'])) ? $this->element['#title'] : $key,
-    ]);
+    $t_args = ['@title' => $this->element['#admin_title'] ?: $this->element['#title']];
+    $form['#title'] = $this->t('Duplicate @title element', $t_args);
 
     $this->action = $this->t('created');
     return parent::buildForm($form, $form_state, $webform, NULL, $element_initialized['#webform_parent_key']);
diff --git a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementEditForm.php b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementEditForm.php
index 333b2c7c2ddf617ebd07787f24cc4a3460e010cf..651dfb321488b3169cc674ddc46da9548d217b0d 100644
--- a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementEditForm.php
+++ b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementEditForm.php
@@ -6,7 +6,6 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Markup;
 use Drupal\Core\Url;
-use Drupal\webform\Utility\WebformDialogHelper;
 use Drupal\webform\WebformInterface;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
@@ -15,6 +14,11 @@
  */
 class WebformUiElementEditForm extends WebformUiElementFormBase {
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $operation = 'update';
+
   /**
    * {@inheritdoc}
    */
@@ -45,45 +49,10 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
 
     $form = parent::buildForm($form, $form_state, $webform, $key);
 
-    // ISSUE:
-    // The below delete link with .use-ajax is throwing errors because the modal
-    // dialog code is creating a <button> without any parent form.
-    // Issue #2879304: Editing Select Other elements produces JavaScript errors
-    // @see Drupal.Ajax
-    /*
-    if ($this->isModalDialog()) {
-      $form['actions']['delete'] = [
-        '#type' => 'link',
-        '#title' => $this->t('Delete'),
-        '#url' => new Url(
-          'entity.webform_ui.element.delete_form',
-          [
-            'webform' => $webform->id(),
-            'key' => $key,
-          ]
-        ),
-        '#attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::NARROW_DIALOG, ['button', 'button--danger']),
-      ];
-    }
-    */
-
-    // WORKAROUND:
-    // Create a hidden link that is clicked using jQuery.
-    if ($this->isDialog()) {
-      $form['delete'] = [
-        '#type' => 'link',
-        '#title' => $this->t('Delete'),
-        '#url' => new Url('entity.webform_ui.element.delete_form', ['webform' => $webform->id(), 'key' => $key]),
-        '#attributes' => ['style' => 'display:none'] + WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW, ['webform-ui-element-delete-link']),
-      ];
-      $form['actions']['delete'] = [
-        '#type' => 'submit',
-        '#value' => $this->t('Delete'),
-        '#attributes' => [
-          'class' => ['button', 'button--danger'],
-          'onclick' => "jQuery('.webform-ui-element-delete-link').click(); return false;",
-        ],
-      ];
+    // Delete action.
+    if (!$form_state->get('default_value_element')) {
+      $url = new Url('entity.webform_ui.element.delete_form', ['webform' => $webform->id(), 'key' => $key]);
+      $this->buildDialogDeleteAction($form, $form_state, $url);
     }
 
     return $form;
diff --git a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementFormBase.php b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementFormBase.php
index cfad8991e28986bcd6c87d9fb4b282e037656455..e06e4251615cb3d8382557e66bca43011a4dc1cc 100644
--- a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementFormBase.php
+++ b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementFormBase.php
@@ -2,13 +2,13 @@
 
 namespace Drupal\webform_ui\Form;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Form\SubformState;
 use Drupal\Core\Render\RendererInterface;
-use Drupal\Core\Serialization\Yaml;
 use Drupal\Core\Url;
 use Drupal\webform\Utility\WebformDialogHelper;
 use Drupal\webform\Form\WebformDialogFormTrait;
@@ -16,6 +16,7 @@
 use Drupal\webform\Utility\WebformYaml;
 use Drupal\webform\WebformEntityElementsValidatorInterface;
 use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformTokenManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -66,6 +67,13 @@ abstract class WebformUiElementFormBase extends FormBase implements WebformUiEle
    */
   protected $elementsValidator;
 
+  /**
+   * The token manager.
+   *
+   * @var \Drupal\webform\WebformTokenManagerInterface
+   */
+  protected $tokenManager;
+
   /**
    * The webform.
    *
@@ -101,6 +109,13 @@ abstract class WebformUiElementFormBase extends FormBase implements WebformUiEle
    */
   protected $originalType;
 
+  /**
+   * The operation of the current webform.
+   *
+   * @var string
+   */
+  protected $operation;
+
   /**
    * The action of the current webform.
    *
@@ -126,12 +141,15 @@ public function getFormId() {
    *   The webform element manager.
    * @param \Drupal\webform\WebformEntityElementsValidatorInterface $elements_validator
    *   Webform element validator.
+   * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
+   *   The webform token manager.
    */
-  public function __construct(RendererInterface $renderer, EntityFieldManagerInterface $entity_field_manager, WebformElementManagerInterface $element_manager, WebformEntityElementsValidatorInterface $elements_validator) {
+  public function __construct(RendererInterface $renderer, EntityFieldManagerInterface $entity_field_manager, WebformElementManagerInterface $element_manager, WebformEntityElementsValidatorInterface $elements_validator, WebformTokenManagerInterface $token_manager) {
     $this->renderer = $renderer;
     $this->entityFieldManager = $entity_field_manager;
     $this->elementManager = $element_manager;
     $this->elementsValidator = $elements_validator;
+    $this->tokenManager = $token_manager;
   }
 
   /**
@@ -142,7 +160,8 @@ public static function create(ContainerInterface $container) {
       $container->get('renderer'),
       $container->get('entity_field.manager'),
       $container->get('plugin.manager.webform.element'),
-      $container->get('webform.elements_validator')
+      $container->get('webform.elements_validator'),
+      $container->get('webform.token_manager')
     );
   }
 
@@ -200,7 +219,7 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
           '#type' => 'link',
           '#title' => $this->t('Cancel'),
           '#url' => new Url('entity.webform_ui.element.edit_form', $route_parameters),
-          '#attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NORMAL, ['button', 'button--small']),
+          '#attributes' => WebformDialogHelper::getOffCanvasDialogAttributes(WebformDialogHelper::DIALOG_NORMAL, ['button', 'button--small']),
         ];
         $form['properties']['element']['type']['#description'] = '(' . $this->t('Changing from %type', ['%type' => $original_webform_element->getPluginLabel()]) . ')';
       }
@@ -270,6 +289,21 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
       '#button_type' => 'primary',
       '#_validate_form' => TRUE,
     ];
+    if ($this->operation === 'create' && $this->isAjax()) {
+      $form['actions']['save_add_element'] = [
+        '#type' => 'submit',
+        '#value' => $this->t('Save + Add element'),
+        '#_validate_form' => TRUE,
+      ];
+    }
+
+    // Add token links below the form and on every tab.
+    $form['token_tree_link'] = $this->tokenManager->buildTreeElement();
+    if ($form['token_tree_link']) {
+      $form['token_tree_link'] += [
+        '#weight' => 101,
+      ];
+    }
 
     $form = $this->buildDefaultValueForm($form, $form_state);
 
@@ -320,7 +354,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
       $t_args = [':href' => Url::fromRoute('entity.webform.source_form', ['webform' => $webform->id()])->toString()];
       $form_state->setErrorByName('elements', $this->t('There has been error validating the elements. You may need to edit the <a href=":href">YAML source</a> to resolve the issue.', $t_args));
       foreach ($messages as $message) {
-        drupal_set_message($message, 'error');
+        $this->messenger()->addError($message);
       }
     }
   }
@@ -329,6 +363,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
+    $op = $form_state->getValue('op');
     $parent_key = $form_state->getValue('parent_key');
     $key = $form_state->getValue('key');
 
@@ -354,18 +389,28 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
       '%title' => (!empty($properties['title'])) ? $properties['title'] : $key,
       '@action' => $this->action,
     ];
-    drupal_set_message($this->t('%title has been @action.', $t_args));
+    $this->messenger()->addStatus($this->t('%title has been @action.', $t_args));
+
+    // Determine add element parent key.
+    $save_and_add_element = ($op == (string) $this->t('Save + Add element')) ? TRUE : FALSE;
+    $add_element = ($element_plugin->isContainer($this->getElement())) ? $key : $parent_key;
+    $add_element = $add_element ? Html::getClass($add_element) : '_root_';
 
     // Append ?update= to (redirect) destination.
     if ($this->requestStack->getCurrentRequest()->query->get('destination')) {
       $redirect_destination = $this->getRedirectDestination();
       $destination = $redirect_destination->get();
       $destination .= (strpos($destination, '?') !== FALSE ? '&' : '?') . 'update=' . $key;
+      $destination .= ($save_and_add_element) ? '&add_element=' . $add_element : '';
       $redirect_destination->set($destination);
     }
 
     // Still set the redirect URL just to be safe.
-    $form_state->setRedirectUrl($this->webform->toUrl('edit-form', ['query' => ['update' => $key]]));
+    $query = ['update' => $key];
+    if ($save_and_add_element) {
+      $query['add_element'] = $add_element;
+    }
+    $form_state->setRedirectUrl($this->webform->toUrl('edit-form', ['query' => $query]));
   }
 
   /**
@@ -459,13 +504,13 @@ public function exists($key) {
   /**
    * Get the default key for the current element.
    *
-   * Default key will be auto incremented when there are  duplicate keys.
+   * Default key will be auto incremented when there are duplicate keys.
    *
    * @return null|string
    *   An element's default key which will be incremented to prevent duplicate
    *   keys.
    */
-  protected function getDefaultKey() {
+  public function getDefaultKey() {
     $element_plugin = $this->getWebformElementPlugin();
     if (empty($element_plugin->getDefaultKey())) {
       return NULL;
@@ -537,9 +582,6 @@ public function buildDefaultValueForm(array &$form, FormStateInterface $form_sta
       $form['properties']['#type'] = 'container';
       $form['properties']['#attributes']['style'] = 'display: none';
 
-      // Add tokens.
-      $form['token_tree_link'] = $form['properties']['token_tree_link'];
-
       // Disable client-side validation.
       $form['#attributes']['novalidate'] = TRUE;
 
@@ -547,11 +589,15 @@ public function buildDefaultValueForm(array &$form, FormStateInterface $form_sta
       $form['actions']['submit'] = [
         '#type' => 'submit',
         '#value' => $this->t('Update default value'),
+        '#attributes' => ['data-hash' => 'webform-tab--advanced'],
         '#validate' => ['::validateDefaultValue'],
         '#submit' => ['::getDefaultValue'],
         '#button_type' => 'primary',
       ];
 
+      // Remove 'Save + Add element'.
+      unset($form['actions']['save_add_element']);
+
       if ($this->isAjax()) {
         $form['actions']['submit']['#ajax'] = [
           'callback' => '::submitAjaxForm',
@@ -561,18 +607,17 @@ public function buildDefaultValueForm(array &$form, FormStateInterface $form_sta
     }
     else {
       // Add 'Set default value' button.
-      $form['properties']['default']['set_default_value'] = [
+      $form['properties']['default']['actions'] = ['#type' => 'container'];
+      $form['properties']['default']['actions']['set_default_value'] = [
         '#type' => 'submit',
         '#value' => $this->t('Set default value'),
         '#submit' => ['::setDefaultValue'],
-        // Disable client-site validation to allow tokens to be posted back to
-        // server.
-        '#attributes' => ['class' => ['js-webform-novalidate']],
+        '#attributes' => ['formnovalidate' => 'formnovalidate'],
         '#_validate_form' => TRUE,
       ];
 
       if ($this->isAjax()) {
-        $form['properties']['default']['set_default_value']['#ajax'] = [
+        $form['properties']['default']['actions']['set_default_value']['#ajax'] = [
           'callback' => '::submitAjaxForm',
           'event' => 'click',
         ];
@@ -601,7 +646,7 @@ public function getDefaultValue(array &$form, FormStateInterface $form_state) {
     $element_plugin = $this->getWebformElementPlugin();
     if (is_array($default_value)) {
       if ($element_plugin->isComposite()) {
-        $default_value = WebformYaml::tidy(Yaml::encode($default_value));
+        $default_value = WebformYaml::encode($default_value);
       }
       else {
         $default_value = implode(', ', $default_value);
diff --git a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTestForm.php b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTestForm.php
index 25fe75825d25509bd46781574faca430ab9cb42c..321f4aef67f438f4fe903e4f0806a9ca91f1e988 100644
--- a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTestForm.php
+++ b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTestForm.php
@@ -20,6 +20,11 @@
  */
 class WebformUiElementTestForm extends WebformUiElementFormBase {
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $operation = 'test';
+
   /**
    * Type of webform element being tested.
    *
@@ -155,7 +160,7 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
       ];
     }
 
-    // Clear all messages including 'Unable to display this webform...' which is
+    // Clear all messages including 'Unable to display this webform…' which is
     // generated because we are using a temp webform.
     // drupal_get_messages();
     return $form;
@@ -166,7 +171,7 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
    */
   public function reset(array &$form, FormStateInterface $form_state) {
     \Drupal::request()->getSession()->remove('webform_ui_test_element_' . $this->type);
-    drupal_set_message($this->t('Webform element %type test has been reset.', ['%type' => $this->type]));
+    $this->messenger()->addStatus($this->t('Webform element %type test has been reset.', ['%type' => $this->type]));
   }
 
   /**
@@ -192,7 +197,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
 
     \Drupal::request()->getSession()->set('webform_ui_test_element_' . $this->type, $properties);
 
-    drupal_set_message($this->t('Webform element %type test has been updated.', ['%type' => $this->type]));
+    $this->messenger()->addStatus($this->t('Webform element %type test has been updated.', ['%type' => $this->type]));
   }
 
   /**
diff --git a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTypeChangeForm.php b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTypeChangeForm.php
index 14163e95bbb5fba8fb8427e2842402704398ae0f..6c49f627957cf5e95ff5c64c31e4d2934d34df90 100644
--- a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTypeChangeForm.php
+++ b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTypeChangeForm.php
@@ -35,7 +35,6 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
     }
 
     $elements = $this->elementManager->getInstances();
-    $definitions = $this->getDefinitions();
 
     $form = parent::buildForm($form, $form_state, $webform);
 
@@ -51,21 +50,20 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
     $form['actions']['cancel'] = [
       '#type' => 'link',
       '#title' => $this->t('Cancel'),
-      '#attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NORMAL, ['button']),
+      '#attributes' => WebformDialogHelper::getOffCanvasDialogAttributes(WebformDialogHelper::DIALOG_NORMAL, ['button']),
       '#url' => Url::fromRoute('entity.webform_ui.element.edit_form', ['webform' => $webform->id(), 'key' => $key]),
     ];
 
     foreach ($related_types as $element_type => $element_type_label) {
       /** @var \Drupal\webform\Plugin\WebformElementInterface $webform_element */
       $webform_element = $elements[$element_type];
-      $plugin_definition = $definitions[$element_type];
 
       $url = Url::fromRoute(
         'entity.webform_ui.element.edit_form',
         ['webform' => $webform->id(), 'key' => $key],
         ['query' => ['type' => $element_type]]
       );
-      $form['elements'][$element_type] = $this->buildRow($plugin_definition, $webform_element, $url, $this->t('Change'));
+      $form['elements'][$element_type] = $this->buildRow($webform_element, $url, $this->t('Change'));
     }
 
     return $form;
diff --git a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTypeFormBase.php b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTypeFormBase.php
index 056c60298046c2e8d2d695386af3c0f470780d59..23647f0d01161a213ff0cb4163cfdb9e48d37943 100644
--- a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTypeFormBase.php
+++ b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTypeFormBase.php
@@ -98,6 +98,7 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
     $form['#prefix'] = '<div id="webform-ui-element-type-ajax-wrapper">';
     $form['#suffix'] = '</div>';
 
+    $form['#attached']['library'][] = 'webform/webform.admin';
     $form['#attached']['library'][] = 'webform/webform.form';
     $form['#attached']['library'][] = 'webform/webform.tooltip';
     $form['#attached']['library'][] = 'webform_ui/webform_ui';
@@ -131,16 +132,28 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
       '#attributes' => [
         'class' => ['webform-form-filter-text'],
         'data-element' => '.webform-ui-element-type-table',
+        'data-item-singlular' => $this->t('element'),
+        'data-item-plural' => $this->t('elements'),
+        'data-no-results' => '.webform-element-no-results',
         'title' => $this->t('Enter a part of the element name to filter by.'),
         'autofocus' => 'autofocus',
       ],
     ];
 
+    // No results.
+    $form['no_results'] = [
+      '#type' => 'webform_message',
+      '#message_message' => $this->t('No elements found. Try a different search.'),
+      '#message_type' => 'info',
+      '#attributes' => ['class' => ['webform-element-no-results']],
+      '#weight' => 1000,
+    ];
+
     return $form;
   }
 
   /**
-   * Never trigge validation.
+   * Never trigger validation.
    */
   public function noValidate(array &$form, FormStateInterface $form_state) {
     $form_state->clearErrors();
@@ -170,8 +183,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
    *   to a URL
    */
   public function submitAjaxForm(array &$form, FormStateInterface $form_state) {
-    // Remove wrapper.
-    unset($form['#prefix'], $form['#suffix']);
+    // Remove #id from wrapper so that the form is still wrapped in a <div>
+    // and triggerable.
+    // @see js/webform.element.details.toggle.js
+    $form['#prefix'] = '<div>';
 
     $response = new AjaxResponse();
     $response->addCommand(new HtmlCommand('#webform-ui-element-type-ajax-wrapper', $form));
@@ -209,8 +224,6 @@ protected function getHeader() {
   /**
    * Build element type row.
    *
-   * @param array $plugin_definition
-   *   Webform element plugin definition.
    * @param \Drupal\webform\Plugin\WebformElementInterface $webform_element
    *   Webform element plugin.
    * @param \Drupal\Core\Url $url
@@ -221,13 +234,13 @@ protected function getHeader() {
    * @return array
    *   A renderable array containing the element type row.
    */
-  protected function buildRow(array $plugin_definition, WebformElementInterface $webform_element, Url $url, $label) {
+  protected function buildRow(WebformElementInterface $webform_element, Url $url, $label) {
     $row = [];
 
     // Type.
     $row['type']['link'] = [
       '#type' => 'link',
-      '#title' => $plugin_definition['label'],
+      '#title' => $webform_element->getPluginLabel(),
       '#url' => $url,
       '#attributes' => WebformDialogHelper::getOffCanvasDialogAttributes(),
       '#prefix' => '<span class="webform-form-filter-text-source">',
@@ -235,7 +248,8 @@ protected function buildRow(array $plugin_definition, WebformElementInterface $w
     ];
     $row['type']['help'] = [
       '#type' => 'webform_help',
-      '#help' => $plugin_definition['description'],
+      '#help' => $webform_element->getPluginDescription(),
+      '#help_title' => $webform_element->getPluginLabel(),
     ];
 
     // Preview.
@@ -265,7 +279,7 @@ protected function buildRow(array $plugin_definition, WebformElementInterface $w
       }
       $row['type']['#attributes']['class'][] = 'js-webform-tooltip-link';
       $row['type']['#attributes']['class'][] = 'webform-tooltip-link';
-      $row['type']['#attributes']['title'] = $plugin_definition['description'];
+      $row['type']['#attributes']['title'] = $webform_element->getPluginDescription();
     }
 
     return $row;
@@ -363,8 +377,10 @@ protected function buildElementPreview(WebformElementInterface $webform_element)
 
     // Custom element type specific attributes.
     switch ($webform_element->getTypeName()) {
+      case 'details':
       case 'fieldset':
       case 'webform_email_confirm':
+        // Title needs to be displayed.
         unset($element['#title_display']);
         break;
 
@@ -416,7 +432,7 @@ protected function buildElementPreview(WebformElementInterface $webform_element)
         ];
         break;
 
-      case 'webform_location':
+      case 'webform_location_geocomplete':
         unset($element['#map'], $element['#geolocation']);
         break;
 
@@ -479,11 +495,7 @@ protected function getDefinitions() {
     foreach ($grouped_definitions as $grouped_definition) {
       $sorted_definitions += $grouped_definition;
     }
-    foreach ($sorted_definitions as &$plugin_definition) {
-      if (empty($plugin_definition['category'])) {
-        $plugin_definition['category'] = $this->t('Other elements');
-      }
-    }
+
     return $sorted_definitions;
   }
 
diff --git a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTypeSelectForm.php b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTypeSelectForm.php
index 87b523a49d5d902e9c79632fb4868a730147f4a8..baadbbb6588642f756e0ab79ab4c2c2b74263596 100644
--- a/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTypeSelectForm.php
+++ b/web/modules/webform/modules/webform_ui/src/Form/WebformUiElementTypeSelectForm.php
@@ -24,6 +24,12 @@ public function getFormId() {
   public function buildForm(array $form, FormStateInterface $form_state, WebformInterface $webform = NULL) {
     $parent = $this->getRequest()->query->get('parent');
 
+    if ($parent) {
+      $parent_element = $webform->getElement($parent);
+      $t_args = ['@parent' => $parent_element['#admin_title'] ?: $parent_element['#title'] ?: $parent_element['#webform_key']];
+      $form['#title'] = $this->t('Select an element to add to "@parent"', $t_args);
+    }
+
     $elements = $this->elementManager->getInstances();
     $definitions = $this->getDefinitions();
     $category_index = 0;
@@ -31,29 +37,27 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
 
     $form = parent::buildForm($form, $form_state, $webform);
 
-    foreach ($definitions as $plugin_id => $plugin_definition) {
-      $element_type = $plugin_id;
-
+    foreach (array_keys($definitions) as $element_type) {
       /** @var \Drupal\webform\Plugin\WebformElementInterface $webform_element */
       $webform_element = $elements[$element_type];
 
-      // Skip hidden plugins.
-      if ($webform_element->isHidden()) {
+      // Skip disabled or hidden.
+      if ($webform_element->isDisabled() || $webform_element->isHidden()) {
         continue;
       }
 
       // Skip wizard page which has a dedicated URL.
-      if ($element_type == 'webform_wizard_page') {
+      if ($element_type === 'webform_wizard_page') {
         continue;
       }
 
-      $category_name = (string) $plugin_definition['category'];
+      $category_name = (string) $webform_element->getPluginCategory();
       if (!isset($categories[$category_name])) {
         $categories[$category_name] = $category_index++;
         $category_id = $categories[$category_name];
         $form[$category_id] = [
           '#type' => 'details',
-          '#title' => $plugin_definition['category'],
+          '#title' => $webform_element->getPluginCategory(),
           '#open' => TRUE,
           '#attributes' => ['data-webform-element-id' => 'webform-ui-element-type-' . $category_id],
         ];
@@ -76,7 +80,7 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
         ['webform' => $webform->id(), 'type' => $element_type],
         ($parent) ? ['query' => ['parent' => $parent]] : []
       );
-      $form[$category_id]['elements'][$element_type] = $this->buildRow($plugin_definition, $webform_element, $url, $this->t('Add element'));
+      $form[$category_id]['elements'][$element_type] = $this->buildRow($webform_element, $url, $this->t('Add element'));
     }
 
     return $form;
diff --git a/web/modules/webform/modules/webform_ui/src/PathProcessor/WebformUiPathProcessor.php b/web/modules/webform/modules/webform_ui/src/PathProcessor/WebformUiPathProcessor.php
index 7df6c74903508ad90146d7df684e43bb0a0893bf..51e1254bc59869e9ef21825d0fd127f9bfc55951 100644
--- a/web/modules/webform/modules/webform_ui/src/PathProcessor/WebformUiPathProcessor.php
+++ b/web/modules/webform/modules/webform_ui/src/PathProcessor/WebformUiPathProcessor.php
@@ -15,7 +15,7 @@ class WebformUiPathProcessor implements OutboundPathProcessorInterface {
    * {@inheritdoc}
    */
   public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) {
-    if (strpos($path, '/webform/') === FALSE  || !method_exists($request, 'getQueryString')) {
+    if (strpos($path, '/webform/') === FALSE || !method_exists($request, 'getQueryString')) {
       return $path;
     }
 
diff --git a/web/modules/webform/modules/webform_ui/src/Tests/WebformUiElementPropertiesTest.php b/web/modules/webform/modules/webform_ui/src/Tests/WebformUiElementPropertiesTest.php
index 571a2024793fb8018f046be723faaacd157f31e7..73e5886adf201198f0b7a52ab53ed478efbc2aae 100644
--- a/web/modules/webform/modules/webform_ui/src/Tests/WebformUiElementPropertiesTest.php
+++ b/web/modules/webform/modules/webform_ui/src/Tests/WebformUiElementPropertiesTest.php
@@ -29,7 +29,7 @@ class WebformUiElementPropertiesTest extends WebformTestBase {
     'example_element_states',
     'test_element',
     'test_element_access',
-    'test_form_states_triggers',
+    'test_states_triggers',
     'test_example_elements',
     'test_example_elements_composite',
   ];
diff --git a/web/modules/webform/modules/webform_ui/src/Tests/WebformUiElementTest.php b/web/modules/webform/modules/webform_ui/src/Tests/WebformUiElementTest.php
index 50e98984cd96e07e48a3a028cb8c8cb5e5ed9bde..629bd9ea0317e8808816f71c68fd87c714d83c01 100644
--- a/web/modules/webform/modules/webform_ui/src/Tests/WebformUiElementTest.php
+++ b/web/modules/webform/modules/webform_ui/src/Tests/WebformUiElementTest.php
@@ -46,23 +46,23 @@ public function testElements() {
     $webform_contact = Webform::load('contact');
 
     /**************************************************************************/
-    // Multiple
+    // Multiple.
     /**************************************************************************/
 
     // Check multiple enabled before submission.
-    $this->drupalGet('admin/structure/webform/manage/contact/element/name/edit');
+    $this->drupalGet('/admin/structure/webform/manage/contact/element/name/edit');
     $this->assertRaw('<select data-drupal-selector="edit-properties-multiple-container-cardinality" id="edit-properties-multiple-container-cardinality" name="properties[multiple][container][cardinality]" class="form-select">');
     $this->assertNoRaw('<em>There is data for this element in the database. This setting can no longer be changed.</em>');
 
     // Check multiple disabled after submission.
     $this->postSubmissionTest($webform_contact);
-    $this->drupalGet('admin/structure/webform/manage/contact/element/name/edit');
+    $this->drupalGet('/admin/structure/webform/manage/contact/element/name/edit');
     $this->assertNoRaw('<select data-drupal-selector="edit-properties-multiple-container-cardinality" id="edit-properties-multiple-container-cardinality" name="properties[multiple][container][cardinality]" class="form-select">');
     $this->assertRaw('<select data-drupal-selector="edit-properties-multiple-container-cardinality" disabled="disabled" id="edit-properties-multiple-container-cardinality" name="properties[multiple][container][cardinality]" class="form-select">');
     $this->assertRaw('<em>There is data for this element in the database. This setting can no longer be changed.</em>');
 
     /**************************************************************************/
-    // Reordering
+    // Reordering.
     /**************************************************************************/
 
     // Check original contact element order.
@@ -83,12 +83,55 @@ public function testElements() {
     $webform_contact = Webform::load('contact');
     $this->assertEqual(['message', 'subject', 'email', 'name', 'actions'], array_keys($webform_contact->getElementsDecodedAndFlattened()));
 
+    /**************************************************************************/
+    // Hierarchy.
+    /**************************************************************************/
+
+    // Create a simple test form.
+    $values = ['id' => 'test'];
+    $elements = [
+      'details_01' => [
+        '#type' => 'details',
+        '#title' => 'details_01',
+        'text_field_01' => [
+          '#type' => 'textfield',
+          '#title' => 'textfield_01',
+        ],
+      ],
+      'details_02' => [
+        '#type' => 'details',
+        '#title' => 'details_02',
+        'text_field_02' => [
+          '#type' => 'textfield',
+          '#title' => 'textfield_02',
+        ],
+      ],
+    ];
+    $this->createWebform($values, $elements);
+    $this->drupalGet('/admin/structure/webform/manage/test');
+
+    // Check setting container to itself displays an error.
+    $edit = [
+      'webform_ui_elements[details_01][parent_key]' => 'details_01',
+    ];
+    $this->drupalPostForm('admin/structure/webform/manage/test', $edit, t('Save elements'));
+    $this->assertRaw('Parent <em class="placeholder">details_01</em> key is not valid.');
+
+    // Check setting containers to one another displays an error.
+    $edit = [
+      'webform_ui_elements[details_01][parent_key]' => 'details_02',
+      'webform_ui_elements[details_02][parent_key]' => 'details_01',
+    ];
+    $this->drupalPostForm('admin/structure/webform/manage/test', $edit, t('Save elements'));
+    $this->assertRaw('Parent <em class="placeholder">details_01</em> key is not valid.');
+    $this->assertRaw('Parent <em class="placeholder">details_02</em> key is not valid.');
+
     /**************************************************************************/
     // Required.
     /**************************************************************************/
 
     // Check name is required.
-    $this->drupalGet('admin/structure/webform/manage/contact');
+    $this->drupalGet('/admin/structure/webform/manage/contact');
     $this->assertFieldChecked('edit-webform-ui-elements-name-required');
 
     // Check name is not required.
@@ -102,6 +145,12 @@ public function testElements() {
     // CRUD
     /**************************************************************************/
 
+    // Check that 'Save + Add element' is only visible in dialogs.
+    $this->drupalGet('/admin/structure/webform/manage/contact/element/add/textfield');
+    $this->assertNoRaw('Save + Add element');
+    $this->drupalGet('/admin/structure/webform/manage/contact/element/add/textfield', ['query' => ['_wrapper_format' => 'drupal_dialog']]);
+    $this->assertRaw('Save + Add element');
+
     // Create element.
     $this->drupalPostForm('admin/structure/webform/manage/contact/element/add/textfield', ['key' => 'test', 'properties[title]' => 'Test'], t('Save'));
 
@@ -119,7 +168,7 @@ public function testElements() {
     $this->assertRaw('The machine-readable name is already in use. It must be unique.');
 
     // Check read element.
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertRaw('<label for="edit-test">Test</label>');
     $this->assertRaw('<input data-drupal-selector="edit-test" type="text" id="edit-test" name="test" value="" size="60" maxlength="255" class="form-text" />');
 
@@ -130,7 +179,7 @@ public function testElements() {
     $this->assertUrl('admin/structure/webform/manage/contact', ['query' => ['update' => 'test']]);
 
     // Check element updated.
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertRaw('<label for="edit-test">Test 123</label>');
     $this->assertRaw('<input data-drupal-selector="edit-test" type="text" id="edit-test" name="test" value="This is a default value" size="60" maxlength="255" class="form-text" />');
 
@@ -140,13 +189,21 @@ public function testElements() {
 
     // Check delete element.
     $this->drupalPostForm('admin/structure/webform/manage/contact/element/test/delete', [], t('Delete'));
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertNoRaw('<label for="edit-test">Test 123</label>');
     $this->assertNoRaw('<input data-drupal-selector="edit-test" type="text" id="edit-test" name="test" value="This is a default value" size="60" maxlength="255" class="form-text" />');
 
     // Check that 'test' element values were deleted from the webform_submission_data table.
     $this->assertEqual(0, \Drupal::database()->query("SELECT COUNT(sid) FROM {webform_submission_data} WHERE webform_id='contact' AND name='test'")->fetchField());
 
+    // Check access allowed to textfield element.
+    $this->drupalGet('/admin/structure/webform/manage/contact/element/add/textfield');
+    $this->assertResponse(200);
+
+    // Check access denied to password element, which is disabled by default.
+    $this->drupalGet('/admin/structure/webform/manage/contact/element/add/password');
+    $this->assertResponse(403);
+
     /**************************************************************************/
     // Change type
     /**************************************************************************/
@@ -155,14 +212,14 @@ public function testElements() {
     $this->drupalPostForm('admin/structure/webform/manage/contact/element/add/textfield', ['key' => 'test', 'properties[title]' => 'Test'], t('Save'));
 
     // Check element type.
-    $this->drupalGet('admin/structure/webform/manage/contact/element/test/edit');
+    $this->drupalGet('/admin/structure/webform/manage/contact/element/test/edit');
     // Check change element type link.
-    $this->assertRaw('Text field<a href="' . $base_path . 'admin/structure/webform/manage/contact/element/test/change" class="button button--small webform-ajax-link" data-dialog-type="modal" data-dialog-options="{&quot;width&quot;:800,&quot;dialogClass&quot;:&quot;webform-modal&quot;}" data-drupal-selector="edit-change-type" id="edit-change-type">Change</a>');
+    $this->assertRaw('Text field<a href="' . $base_path . 'admin/structure/webform/manage/contact/element/test/change" class="button button--small webform-ajax-link" data-dialog-type="modal" data-dialog-options="{&quot;width&quot;:800,&quot;dialogClass&quot;:&quot;webform-ui-dialog&quot;}" data-drupal-selector="edit-change-type" id="edit-change-type">Change</a>');
     // Check text field has description.
     $this->assertRaw(t('A short description of the element used as help for the user when he/she uses the webform.'));
 
     // Check change element types.
-    $this->drupalGet('admin/structure/webform/manage/contact/element/test/change');
+    $this->drupalGet('/admin/structure/webform/manage/contact/element/test/change');
     $this->assertRaw(t('Hidden'));
     $this->assertCssSelect('a[href$="admin/structure/webform/manage/contact/element/test/edit?type=hidden"][data-dialog-type][data-dialog-options][data-drupal-selector="edit-elements-hidden-operation"]');
     $this->assertRaw(t('value'));
@@ -175,24 +232,24 @@ public function testElements() {
     $this->assertCssSelect('a[href$="admin/structure/webform/manage/contact/element/test/edit?type=url"][data-dialog-type][data-dialog-options][data-drupal-selector="edit-elements-url-operation"]');
 
     // Check change element type.
-    $this->drupalGet('admin/structure/webform/manage/contact/element/test/edit', ['query' => ['type' => 'value']]);
+    $this->drupalGet('/admin/structure/webform/manage/contact/element/test/edit', ['query' => ['type' => 'value']]);
     // Check value has no description.
     $this->assertNoRaw(t('A short description of the element used as help for the user when he/she uses the webform.'));
-    $this->assertRaw('Value<a href="' . $base_path . 'admin/structure/webform/manage/contact/element/test/edit" class="button button--small webform-ajax-link" data-dialog-type="modal" data-dialog-options="{&quot;width&quot;:800,&quot;dialogClass&quot;:&quot;webform-modal&quot;}" data-drupal-selector="edit-cancel" id="edit-cancel">Cancel</a>');
+    $this->assertRaw('Value<a href="' . $base_path . 'admin/structure/webform/manage/contact/element/test/edit" class="button button--small webform-ajax-link" data-dialog-type="dialog" data-dialog-renderer="off_canvas" data-dialog-options="{&quot;width&quot;:600,&quot;dialogClass&quot;:&quot;ui-dialog-off-canvas webform-off-canvas&quot;}" data-drupal-selector="edit-cancel" id="edit-cancel">Cancel</a>');
     $this->assertRaw('(Changing from <em class="placeholder">Text field</em>)');
 
     // Change the element type.
     $this->drupalPostForm('admin/structure/webform/manage/contact/element/test/edit', [], t('Save'), ['query' => ['type' => 'value']]);
 
     // Change the element type from 'textfield' to 'value'.
-    $this->drupalGet('admin/structure/webform/manage/contact/element/test/edit');
+    $this->drupalGet('/admin/structure/webform/manage/contact/element/test/edit');
 
     // Check change element type link.
-    $this->assertRaw('Value<a href="' . $base_path . 'admin/structure/webform/manage/contact/element/test/change" class="button button--small webform-ajax-link" data-dialog-type="modal" data-dialog-options="{&quot;width&quot;:800,&quot;dialogClass&quot;:&quot;webform-modal&quot;}" data-drupal-selector="edit-change-type" id="edit-change-type">Change</a>');
+    $this->assertRaw('Value<a href="' . $base_path . 'admin/structure/webform/manage/contact/element/test/change" class="button button--small webform-ajax-link" data-dialog-type="modal" data-dialog-options="{&quot;width&quot;:800,&quot;dialogClass&quot;:&quot;webform-ui-dialog&quot;}" data-drupal-selector="edit-change-type" id="edit-change-type">Change</a>');
 
     // Check color element that does not have related type and return 404.
     $this->drupalPostForm('admin/structure/webform/manage/contact/element/add/color', ['key' => 'test_color', 'properties[title]' => 'Test color'], t('Save'));
-    $this->drupalGet('admin/structure/webform/manage/contact/element/test_color/change');
+    $this->drupalGet('/admin/structure/webform/manage/contact/element/test_color/change');
     $this->assertResponse(404);
 
     /**************************************************************************/
@@ -217,7 +274,7 @@ public function testPermissions() {
     // permission.
     $account = $this->drupalCreateUser(['administer webform']);
     $this->drupalLogin($account);
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/source');
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/source');
     $this->assertResponse(403);
     $this->drupalLogout();
 
@@ -225,7 +282,7 @@ public function testPermissions() {
     // without 'administer webform' permission.
     $account = $this->drupalCreateUser(['edit webform source']);
     $this->drupalLogin($account);
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/source');
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/source');
     $this->assertResponse(403);
     $this->drupalLogout();
 
@@ -233,7 +290,7 @@ public function testPermissions() {
     // and 'administer webform' permission.
     $account = $this->drupalCreateUser(['administer webform', 'edit webform source']);
     $this->drupalLogin($account);
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/source');
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/source');
     $this->assertResponse(200);
     $this->drupalLogout();
   }
diff --git a/web/modules/webform/modules/webform_ui/src/WebformUiEntityElementsForm.php b/web/modules/webform/modules/webform_ui/src/WebformUiEntityElementsForm.php
index 3e921a2eef7a0d94daa9b93db0e6d194f4e71889..f134419223d5de608c6551beae1f42067744a276 100644
--- a/web/modules/webform/modules/webform_ui/src/WebformUiEntityElementsForm.php
+++ b/web/modules/webform/modules/webform_ui/src/WebformUiEntityElementsForm.php
@@ -25,6 +25,18 @@ class WebformUiEntityElementsForm extends BundleEntityFormBase {
 
   use WebformEntityAjaxFormTrait;
 
+  /**
+   * Array of required states.
+   *
+   * @var array
+   */
+  protected $requiredStates = [
+    'required' => 'required',
+    '!required' => '!required',
+    'optional' => 'optional',
+    '!optional' => '!optional',
+  ];
+
   /**
    * The renderer.
    *
@@ -100,74 +112,41 @@ public function buildForm(array $form, FormStateInterface $form_state) {
 
     $header = $this->getTableHeader();
 
-    // Build table rows for elements.
-    $rows = [];
     $elements = $this->getOrderableElements();
+
+    // Get (weight) delta parent options.
     $delta = count($elements);
+    $parent_options = $this->getParentOptions($elements);
+
+    // Build table rows for elements.
+    $rows = [];
     foreach ($elements as $element) {
-      $rows[$element['#webform_key']] = $this->getElementRow($element, $delta);
-    }
-
-    // Must manually add local actions to the webform because we can't alter local
-    // actions and add the needed dialog attributes.
-    // @see https://www.drupal.org/node/2585169
-    $local_actions = [];
-    $local_actions['add_element'] = [
-      '#theme' => 'menu_local_action',
-      '#link' => [
-        'title' => $this->t('Add element'),
-        'url' => new Url('entity.webform_ui.element', ['webform' => $webform->id()]),
-        'attributes' => WebformDialogHelper::getModalDialogAttributes(),
-      ],
-    ];
-    if ($this->elementManager->createInstance('webform_wizard_page')->isEnabled()) {
-      $local_actions['add_page'] = [
-        '#theme' => 'menu_local_action',
-        '#link' => [
-          'title' => $this->t('Add page'),
-          'url' => new Url('entity.webform_ui.element.add_form', ['webform' => $webform->id(), 'type' => 'webform_wizard_page']),
-          'attributes' => WebformDialogHelper::getOffCanvasDialogAttributes(),
-        ],
-      ];
+      $rows[$element['#webform_key']] = $this->getElementRow($element, $delta, $parent_options);
     }
-    if ($webform->hasFlexboxLayout()) {
-      $local_actions['add_layout'] = [
-        '#theme' => 'menu_local_action',
-        '#link' => [
-          'title' => $this->t('Add layout'),
-          'url' => new Url('entity.webform_ui.element.add_form', ['webform' => $webform->id(), 'type' => 'webform_flexbox']),
-          'attributes' => WebformDialogHelper::getOffCanvasDialogAttributes(),
-        ],
-      ];
-    }
-    $form['local_actions'] = [
-      '#prefix' => '<ul class="action-links">',
-      '#suffix' => '</ul>',
-    ] + $local_actions;
 
     $form['webform_ui_elements'] = [
-      '#type' => 'table',
-      '#header' => $header,
-      '#empty' => $this->t('Please add elements to this webform.'),
-      '#attributes' => [
-        'class' => ['webform-ui-elements-table'],
-      ],
-      '#tabledrag' => [
-        [
-          'action' => 'match',
-          'relationship' => 'parent',
-          'group' => 'row-parent-key',
-          'source' => 'row-key',
-          'hidden' => TRUE, /* hides the WEIGHT & PARENT tree columns below */
-          'limit' => FALSE,
+        '#type' => 'table',
+        '#header' => $header,
+        '#empty' => $this->t('Please add elements to this webform.'),
+        '#attributes' => [
+          'class' => ['webform-ui-elements-table'],
         ],
-        [
-          'action' => 'order',
-          'relationship' => 'sibling',
-          'group' => 'row-weight',
+        '#tabledrag' => [
+          [
+            'action' => 'match',
+            'relationship' => 'parent',
+            'group' => 'row-parent-key',
+            'source' => 'row-key',
+            'hidden' => TRUE, /* hides the WEIGHT & PARENT tree columns below */
+            'limit' => FALSE,
+          ],
+          [
+            'action' => 'order',
+            'relationship' => 'sibling',
+            'group' => 'row-weight',
+          ],
         ],
-      ],
-    ] + $rows;
+      ] + $rows;
 
     if ($rows && !$webform->hasActions()) {
       $form['webform_ui_elements'] += ['webform_actions_default' => $this->getCustomizeActionsRow()];
@@ -175,6 +154,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
 
     // Must preload libraries required by (modal) dialogs.
     WebformDialogHelper::attachLibraries($form);
+    $form['#attached']['library'][] = 'webform/webform.admin.tabledrag';
     $form['#attached']['library'][] = 'webform_ui/webform_ui';
 
     $form = parent::buildForm($form, $form_state);
@@ -206,16 +186,37 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
 
     // Validate parent key and add children to ordered elements.
     foreach ($webform_ui_elements as $key => $table_element) {
-      $parent_key = $table_element['parent_key'];
 
-      // Validate the parent key.
-      if ($parent_key && !isset($elements_flattened[$parent_key])) {
-        $form_state->setError($form['webform_ui_elements'], $this->t('Parent %parent_key does not exist.', ['%parent_key' => $parent_key]));
-        return;
+      // Validate parent key.
+      if ($parent_key = $table_element['parent_key']) {
+        // Validate missing parent key.
+        if (!isset($elements_flattened[$parent_key])) {
+          $form_state->setError($form['webform_ui_elements'][$key]['parent']['parent_key'], $this->t('Parent %parent_key does not exist.', ['%parent_key' => $parent_key]));
+          continue;
+        }
+
+        // Validate the parent keys and make sure there
+        // are no recursive parents.
+        $parent_keys = [$key];
+        $current_parent_key = $parent_key;
+        while ($current_parent_key) {
+          if (in_array($current_parent_key, $parent_keys)) {
+            $form_state->setError($form['webform_ui_elements'][$key]['parent']['parent_key'], $this->t('Parent %parent_key key is not valid.', ['%parent_key' => $parent_key]));
+            break;
+          }
+
+          $parent_keys[] = $current_parent_key;
+          $current_parent_key = (isset($webform_ui_elements[$current_parent_key]['parent_key'])) ? $webform_ui_elements[$current_parent_key]['parent_key'] : NULL;
+        }
       }
 
       // Set #required or remove the property.
-      if (isset($webform_ui_elements[$key]['required'])) {
+      $is_conditionally_required = isset($elements_flattened[$key]['#states']) && array_intersect_key($this->requiredStates, $elements_flattened[$key]['#states']);
+      if ($is_conditionally_required) {
+        // Always unset conditionally required elements.
+        unset($elements_flattened[$key]['#required']);
+      }
+      elseif (isset($webform_ui_elements[$key]['required'])) {
         if (empty($webform_ui_elements[$key]['required'])) {
           unset($elements_flattened[$key]['#required']);
         }
@@ -228,6 +229,10 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
       $webform_ui_elements[$parent_key]['children'][$key] = $key;
     }
 
+    if ($form_state->hasAnyErrors()) {
+      return;
+    }
+
     // Rebuild elements to reflect new hierarchy.
     $elements_updated = [];
     // Preserve the original elements root properties.
@@ -242,6 +247,21 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
 
     // Update the webform's elements.
     $webform->setElements($elements_updated);
+
+    // Validate only elements required, hierarchy, and rendering.
+    $validate_options = [
+      'required' => TRUE,
+      'yaml' => FALSE,
+      'array' => FALSE,
+      'names' => FALSE,
+      'properties' => FALSE,
+      'submissions' => FALSE,
+      'hierarchy' => TRUE,
+      'rendering' => TRUE,
+    ];
+    if ($this->elementsValidator->validate($webform, $validate_options)) {
+      $form_state->setErrorByName(NULL, $this->t('There has been error validating the elements.'));
+    }
   }
 
   /**
@@ -259,7 +279,7 @@ public function save(array $form, FormStateInterface $form_state) {
     ];
     $t_args = ['%label' => $webform->label()];
     $this->logger('webform')->notice('Webform @label elements saved.', $context);
-    drupal_set_message($this->t('Webform %label elements saved.', $t_args));
+    $this->messenger()->addStatus($this->t('Webform %label elements saved.', $t_args));
   }
 
   /**
@@ -382,8 +402,14 @@ protected function getTableHeader() {
       'data' => $this->t('Required'),
       'class' => ['webform-ui-element-required', RESPONSIVE_PRIORITY_LOW],
     ];
-    $header['weight'] = $this->t('Weight');
-    $header['parent'] = $this->t('Parent');
+    $header['weight'] = [
+      'data' => $this->t('Weight'),
+      'class' => ['webform-tabledrag-hide'],
+    ];
+    $header['parent'] = [
+      'data' => $this->t('Parent'),
+      'class' => ['webform-tabledrag-hide'],
+    ];
     $header['operations'] = [
       'data' => $this->t('Operations'),
       'class' => ['webform-ui-element-operations'],
@@ -391,18 +417,41 @@ protected function getTableHeader() {
     return $header;
   }
 
+  /**
+   * Get parent (container) elements as options.
+   *
+   * @param array $elements
+   *   A flattened array of elements.
+   *
+   * @return array
+   *   Parent (container) elements as options.
+   */
+  protected function getParentOptions(array $elements) {
+    $options = [];
+    foreach ($elements as $key => $element) {
+      $plugin_id = $this->elementManager->getElementPluginId($element);
+      $webform_element = $this->elementManager->createInstance($plugin_id);
+      if ($webform_element->isContainer($element)) {
+        $options[$key] = $element['#admin_title'] ?: $element['#title'];
+      }
+    }
+    return $options;
+  }
+
   /**
    * Gets an row for a single element.
    *
    * @param array $element
    *   Webform element.
    * @param int $delta
-   *   The number of elements. @todo is this correct?
+   *   The number of elements.
+   * @param array $parent_options
+   *   An associative array of parent (container) options.
    *
    * @return array
    *   The row for the element.
    */
-  protected function getElementRow(array $element, $delta) {
+  protected function getElementRow(array $element, $delta, array $parent_options) {
     /** @var \Drupal\webform\WebformInterface $webform */
     $webform = $this->getEntity();
 
@@ -411,7 +460,8 @@ protected function getElementRow(array $element, $delta) {
     $element_state_options = OptGroup::flattenOptions(WebformElementStates::getStateOptions());
     $element_dialog_attributes = WebformDialogHelper::getOffCanvasDialogAttributes();
     $key = $element['#webform_key'];
-
+    $title = $element['#admin_title'] ?: $element['#title'];
+    $title = (is_array($title)) ? $this->renderer->render($title) : $title;
     $plugin_id = $this->elementManager->getElementPluginId($element);
 
     /** @var \Drupal\webform\Plugin\WebformElementInterface $webform_element */
@@ -499,10 +549,12 @@ protected function getElementRow(array $element, $delta) {
       ];
     }
 
+    $is_conditionally_required = FALSE;
     if ($webform->hasConditions()) {
       $states = [];
       if (!empty($element['#states'])) {
         $states = array_intersect_key($element_state_options, $element['#states']);
+        $is_conditionally_required = array_intersect_key($this->requiredStates, $element['#states']);
       }
       $row['conditional'] = [
         '#type' => 'link',
@@ -515,6 +567,8 @@ protected function getElementRow(array $element, $delta) {
           // Add custom hash to current page's location.
           // @see Drupal.behaviors.webformAjaxLink
           'data-hash' => 'webform-tab--conditions',
+          'title' => $this->t('Edit @states conditional', ['@states' => implode('; ', $states)]),
+          'aria-label' => $this->t('Edit @states conditional', ['@states' => implode('; ', $states)]),
         ],
       ];
     }
@@ -522,8 +576,14 @@ protected function getElementRow(array $element, $delta) {
     if ($webform_element->hasProperty('required')) {
       $row['required'] = [
         '#type' => 'checkbox',
+        '#title' => $this->t('Required for @title', ['@title' => $title]),
+        '#title_display' => 'invisible',
         '#default_value' => (empty($element['#required'])) ? FALSE : TRUE,
       ];
+      if ($is_conditionally_required) {
+        $row['required']['#default_value'] = TRUE;
+        $row['required']['#disabled'] = TRUE;
+      }
     }
     else {
       $row['required'] = ['#markup' => ''];
@@ -531,15 +591,19 @@ protected function getElementRow(array $element, $delta) {
 
     $row['weight'] = [
       '#type' => 'weight',
-      '#title' => $this->t('Weight for ID @id', ['@id' => $key]),
+      '#title' => $this->t('Weight for @title', ['@title' => $title]),
       '#title_display' => 'invisible',
       '#default_value' => $element['#weight'],
+      '#wrapper_attributes' => ['class' => ['webform-tabledrag-hide']],
       '#attributes' => [
         'class' => ['row-weight'],
       ],
       '#delta' => $delta,
     ];
 
+    $row['parent'] = [
+      '#wrapper_attributes' => ['class' => ['webform-tabledrag-hide']],
+    ];
     $row['parent']['key'] = [
       '#parents' => ['webform_ui_elements', $key, 'key'],
       '#type' => 'hidden',
@@ -548,18 +612,30 @@ protected function getElementRow(array $element, $delta) {
         'class' => ['row-key'],
       ],
     ];
-    $row['parent']['parent_key'] = [
-      '#parents' => ['webform_ui_elements', $key, 'parent_key'],
-      '#type' => 'textfield',
-      '#size' => 20,
-      '#title' => $this->t('Parent'),
-      '#title_display' => 'invisible',
-      '#default_value' => $element['#webform_parent_key'],
-      '#attributes' => [
-        'class' => ['row-parent-key'],
-        'readonly' => 'readonly',
-      ],
-    ];
+    if ($parent_options) {
+      $row['parent']['parent_key'] = [
+        '#parents' => ['webform_ui_elements', $key, 'parent_key'],
+        '#type' => 'select',
+        '#options' => $parent_options,
+        '#empty_option' => '',
+        '#title' => $this->t('Parent element @title', ['@title' => $title]),
+        '#title_display' => 'invisible',
+        '#default_value' => $element['#webform_parent_key'],
+        '#attributes' => [
+          'class' => ['row-parent-key'],
+        ],
+      ];
+    }
+    else {
+      $row['parent']['parent_key'] = [
+        '#parents' => ['webform_ui_elements', $key, 'parent_key'],
+        '#type' => 'hidden',
+        '#default_value' => '',
+        '#attributes' => [
+          'class' => ['row-parent-key'],
+        ],
+      ];
+    }
 
     $row['operations'] = [
       '#type' => 'operations',
@@ -643,8 +719,8 @@ protected function getCustomizeActionsRow() {
       $row['conditions'] = ['#markup' => ''];
     }
     $row['required'] = ['#markup' => ''];
-    $row['weight'] = ['#markup' => ''];
-    $row['parent'] = ['#markup' => ''];
+    $row['weight'] = ['#markup' => '', '#wrapper_attributes' => ['class' => ['webform-tabledrag-hide']]];
+    $row['parent'] = ['#markup' => '', '#wrapper_attributes' => ['class' => ['webform-tabledrag-hide']]];
     $row['operations'] = [
       '#type' => 'operations',
       '#prefix' => '<div class="webform-dropbutton">',
diff --git a/web/modules/webform/modules/webform_ui/src/WebformUiOptionsForm.php b/web/modules/webform/modules/webform_ui/src/WebformUiOptionsForm.php
index 125dd4e66210a84123d8984ff08368e086a69cef..4dba09de079067c61beb23207e9cb004d0f44781 100644
--- a/web/modules/webform/modules/webform_ui/src/WebformUiOptionsForm.php
+++ b/web/modules/webform/modules/webform_ui/src/WebformUiOptionsForm.php
@@ -23,7 +23,7 @@ public function editForm(array $form, FormStateInterface $form_state) {
       '#description' => $this->t("Descriptions, which are only applicable to radios and checkboxes, can be delimited using ' -- '."),
       '#description_display' => 'before',
       '#empty_options' => 10,
-      '#add_more' => 10,
+      '#add_more_items' => 10,
       '#default_value' => $this->getOptions(),
     ];
     return $form;
diff --git a/web/modules/webform/modules/webform_ui/webform_ui.info.yml b/web/modules/webform/modules/webform_ui/webform_ui.info.yml
index c2eb16a11521db45e5618e0b6be4d462f4832a25..6ecc37c448fe99c6550169c80fbf45d504449ca5 100644
--- a/web/modules/webform/modules/webform_ui/webform_ui.info.yml
+++ b/web/modules/webform/modules/webform_ui/webform_ui.info.yml
@@ -6,8 +6,8 @@ package: Webform
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/modules/webform_ui/webform_ui.libraries.yml b/web/modules/webform/modules/webform_ui/webform_ui.libraries.yml
index cb2e05cd71ed1db5a334be80a75900e14765a715..fc1abb8a13e105061575a6a63a40bce75f270f79 100644
--- a/web/modules/webform/modules/webform_ui/webform_ui.libraries.yml
+++ b/web/modules/webform/modules/webform_ui/webform_ui.libraries.yml
@@ -4,4 +4,4 @@ webform_ui:
     theme:
       css/webform_ui.module.css: {}
   js:
-    js/webform_ui.js:  {}
+    js/webform_ui.js: {}
diff --git a/web/modules/webform/modules/webform_ui/webform_ui.links.action.yml b/web/modules/webform/modules/webform_ui/webform_ui.links.action.yml
new file mode 100644
index 0000000000000000000000000000000000000000..99b05d9de29fc007a5b4157ddbe0c5d7081ace33
--- /dev/null
+++ b/web/modules/webform/modules/webform_ui/webform_ui.links.action.yml
@@ -0,0 +1,29 @@
+entity.webform_ui.element:
+  route_name: entity.webform_ui.element
+  title: 'Add element'
+  class: '\Drupal\webform\Plugin\Menu\LocalAction\WebformDialogLocalAction'
+  dialog: normal
+  attributes:
+    id: 'webform-ui-add-element'
+  appears_on:
+    - entity.webform.edit_form
+
+entity.webform_ui.element.page:
+  route_name: entity.webform_ui.element.add_page
+  title: 'Add page'
+  class: '\Drupal\webform\Plugin\Menu\LocalAction\WebformDialogLocalAction'
+  off_canvas: normal
+  attributes:
+    id: 'webform-ui-add-page'
+  appears_on:
+    - entity.webform.edit_form
+
+entity.webform_ui.element.layout:
+  route_name: entity.webform_ui.element.add_layout
+  title: 'Add layout'
+  class: '\Drupal\webform\Plugin\Menu\LocalAction\WebformDialogLocalAction'
+  off_canvas: normal
+  attributes:
+    id: 'webform-ui-add-layout'
+  appears_on:
+    - entity.webform.edit_form
diff --git a/web/modules/webform/modules/webform_ui/webform_ui.module b/web/modules/webform/modules/webform_ui/webform_ui.module
index ffe2d631acbb7888d187c61b0f0aacb00e015ed4..bda58fa26df21b870bd638d233bb1a109b8faf12 100644
--- a/web/modules/webform/modules/webform_ui/webform_ui.module
+++ b/web/modules/webform/modules/webform_ui/webform_ui.module
@@ -5,9 +5,6 @@
  * Provides a simple user interface for building and maintaining webforms.
  */
 
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\webform\Utility\WebformDialogHelper;
-
 /**
  * Implements hook_entity_type_alter().
  */
@@ -43,16 +40,20 @@ function webform_ui_entity_type_alter(array &$entity_types) {
 }
 
 /**
- * Implements hook_webform_submission_form_alter().
+ * Implements hook_preprocess_menu_local_action().
+ *
+ * Add .button--secondary to add page and layout actions.
+ *
+ * @see Drupal.behaviors.webformUiElementsActionsSecondary
  */
-function webform_ui_webform_submission_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
-  // Attach dialog libraries to editable webforms to make sure that the
-  // quickedit dialog/system tray works as expected.
-  if (\Drupal::moduleHandler()->moduleExists('quickedit')) {
-    /** @var \Drupal\webform\WebformSubmissionForm $form_object */
-    $form_object = $form_state->getFormObject();
-    if ($form_object->getEntity()->access('update')) {
-      WebformDialogHelper::attachLibraries($form);
-    }
+function webform_ui_preprocess_menu_local_action(&$variables) {
+  if (\Drupal::routeMatch()->getRouteName() != 'entity.webform.edit_form') {
+    return;
+  }
+
+  if (!in_array($variables['link']['#url']->getRouteName(), ['entity.webform_ui.element.add_page', 'entity.webform_ui.element.add_layout'])) {
+    return;
   }
+
+  $variables['link']['#options']['attributes']['class'][] = 'button--secondary';
 }
diff --git a/web/modules/webform/modules/webform_ui/webform_ui.permissions.yml b/web/modules/webform/modules/webform_ui/webform_ui.permissions.yml
deleted file mode 100644
index 739141a4650b1504821937c003cc00189fa1d730..0000000000000000000000000000000000000000
--- a/web/modules/webform/modules/webform_ui/webform_ui.permissions.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-'edit webform source':
-  title: 'Edit webform source code'
-  description: 'Editing webform source code allows users alter and possibly break a webform''s render array.'
-  restrict access: true
diff --git a/web/modules/webform/modules/webform_ui/webform_ui.routing.yml b/web/modules/webform/modules/webform_ui/webform_ui.routing.yml
index 872a46f94898991b49a3ada0183edb5bd31c636b..6926709ce34b4a4a07783cd9128106e24f93a1cf 100644
--- a/web/modules/webform/modules/webform_ui/webform_ui.routing.yml
+++ b/web/modules/webform/modules/webform_ui/webform_ui.routing.yml
@@ -41,7 +41,25 @@ entity.webform_ui.element.add_form:
     _form: '\Drupal\webform_ui\Form\WebformUiElementAddForm'
     _title: 'Add element'
   requirements:
-    _custom_access: '\Drupal\webform_ui\Access\WebformUiAccess::checkWebformEditAccess'
+    _custom_access: '\Drupal\webform_ui\Access\WebformUiAccess::checkWebformElementAccess'
+
+entity.webform_ui.element.add_page:
+  path: '/admin/structure/webform/manage/{webform}/element/add/page'
+  defaults:
+    _form: '\Drupal\webform_ui\Form\WebformUiElementAddForm'
+    _title: 'Add page'
+    type: webform_wizard_page
+  requirements:
+    _custom_access: '\Drupal\webform_ui\Access\WebformUiAccess::checkWebformElementAccess'
+
+entity.webform_ui.element.add_layout:
+  path: '/admin/structure/webform/manage/{webform}/element/add/layout'
+  defaults:
+    _form: '\Drupal\webform_ui\Form\WebformUiElementAddForm'
+    _title: 'Add layout'
+    type: webform_flexbox
+  requirements:
+    _custom_access: '\Drupal\webform_ui\Access\WebformUiAccess::checkWebformElementAccess'
 
 entity.webform_ui.element.edit_form:
   path: '/admin/structure/webform/manage/{webform}/element/{key}/edit'
@@ -67,8 +85,8 @@ entity.webform_ui.element.delete_form:
   requirements:
     _custom_access: '\Drupal\webform_ui\Access\WebformUiAccess::checkWebformEditAccess'
 
-webform.element_plugins.test:
-  path: '/admin/structure/webform/config/elements/{type}/test'
+webform.reports_plugins.elements.test:
+  path: '/admin/reports/webform-plugins/elements/{type}/test'
   defaults:
     _form: '\Drupal\webform_ui\Form\WebformUiElementTestForm'
     _title: 'Test element'
diff --git a/web/modules/webform/reports/accessiblity/text/example_accessibility_advanced.txt b/web/modules/webform/reports/accessiblity/text/example_accessibility_advanced.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7678b6d50c612317bf3903d4cea6da873f4d4b56
--- /dev/null
+++ b/web/modules/webform/reports/accessiblity/text/example_accessibility_advanced.txt
@@ -0,0 +1,89 @@
+
+Welcome to Pa11y
+
+ > Running Pa11y on URL http://localhost/wf/webform/example_accessibility_advanced
+
+Results for URL: http://localhost/wf/webform/example_accessibility_advanced
+
+ • Error: This searchinput element does not have a name available to an accessibility API. Valid names are: label element, title undefined, aria-label undefined, aria-labelledby undefined.
+   ├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.InputSearch.Name
+   ├── #edit-option-elements > div > div:nth-child(2) > span > span:nth-child(1) > span > ul > li > input
+   └── <input class="select2-search__field" type="search" tabindex="0" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" role="textbox" aria-autocomplete="list" placeholder="" style="width: 0.75em;">
+
+ • Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.87:1. Recommendation: change text colour to #757575.
+   ├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
+   ├── #edit-webform-likert-table-q1-likert-question > label
+   └── <label>Please answer question 1?</label>
+
+ • Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.87:1. Recommendation: change text colour to #757575.
+   ├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
+   ├── #edit-webform-likert-table-q2-likert-question > label
+   └── <label>How about now answering questio...</label>
+
+ • Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.87:1. Recommendation: change text colour to #757575.
+   ├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
+   ├── #edit-webform-likert-table-q3-likert-question > label
+   └── <label>Finally, here is question 3?</label>
+
+ • Error: Duplicate id attribute value "iti-item-gb" found on the web page.
+   ├── WCAG2AA.Principle4.Guideline4_1.4_1_1.F77
+   ├── #iti-item-gb
+   └── <li class="country standard" id="iti-item-gb" role="option" data-dial-code="44" data-country-code="gb"><div class="flag-box"><div clas...</li>
+
+ • Error: Duplicate id attribute value "iti-item-us" found on the web page.
+   ├── WCAG2AA.Principle4.Guideline4_1.4_1_1.F77
+   ├── #iti-item-us
+   └── <li class="country standard" id="iti-item-us" role="option" data-dial-code="1" data-country-code="us"><div class="flag-box"><div clas...</li>
+
+ • Error: This link points to a named anchor "terms" within the document, but no anchor exists with that name.
+   ├── WCAG2AA.Principle2.Guideline2_4.2_4_1.G1,G123,G124.NoSuchID
+   ├── #edit-widget-elements > div > div:nth-child(4) > label > a
+   └── <a role="button" href="#terms">terms of service</a>
+
+ • Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.87:1. Recommendation: change text colour to #757575.
+   ├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
+   ├── #edit-tableselect > tbody > tr:nth-child(1) > td:nth-child(2)
+   └── <td>One</td>
+
+ • Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.87:1. Recommendation: change text colour to #757575.
+   ├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
+   ├── #edit-tableselect > tbody > tr:nth-child(2) > td:nth-child(2)
+   └── <td>Two</td>
+
+ • Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.87:1. Recommendation: change text colour to #757575.
+   ├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
+   ├── #edit-tableselect > tbody > tr:nth-child(3) > td:nth-child(2)
+   └── <td>Three</td>
+
+ • Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.87:1. Recommendation: change text colour to #757575.
+   ├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
+   ├── #edit-webform-table-sort > tbody > tr:nth-child(1) > td:nth-child(1)
+   └── <td><a href="#" class="tabledrag-ha...</td>
+
+ • Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.87:1. Recommendation: change text colour to #757575.
+   ├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
+   ├── #edit-webform-table-sort > tbody > tr:nth-child(2) > td:nth-child(1)
+   └── <td><a href="#" class="tabledrag-ha...</td>
+
+ • Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.87:1. Recommendation: change text colour to #757575.
+   ├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
+   ├── #edit-webform-table-sort > tbody > tr:nth-child(3) > td:nth-child(1)
+   └── <td><a href="#" class="tabledrag-ha...</td>
+
+ • Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.87:1. Recommendation: change text colour to #757575.
+   ├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
+   ├── #edit-webform-tableselect-sort > tbody > tr:nth-child(1) > td:nth-child(2)
+   └── <td>One</td>
+
+ • Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.87:1. Recommendation: change text colour to #757575.
+   ├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
+   ├── #edit-webform-tableselect-sort > tbody > tr:nth-child(2) > td:nth-child(2)
+   └── <td>Two</td>
+
+ • Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.87:1. Recommendation: change text colour to #757575.
+   ├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
+   ├── #edit-webform-tableselect-sort > tbody > tr:nth-child(3) > td:nth-child(2)
+   └── <td>Three</td>
+
+16 Errors
+
diff --git a/web/modules/webform/reports/accessiblity/text/example_accessibility_basic.txt b/web/modules/webform/reports/accessiblity/text/example_accessibility_basic.txt
new file mode 100644
index 0000000000000000000000000000000000000000..587b594f2f169cbeba9ba7c7532c581a239062a5
--- /dev/null
+++ b/web/modules/webform/reports/accessiblity/text/example_accessibility_basic.txt
@@ -0,0 +1,19 @@
+
+Welcome to Pa11y
+
+ > Running Pa11y on URL http://localhost/wf/webform/example_accessibility_basic
+
+Results for URL: http://localhost/wf/webform/example_accessibility_basic
+
+ • Error: Duplicate id attribute value "edit-managed-file-upload" found on the web page.
+   ├── WCAG2AA.Principle4.Guideline4_1.4_1_1.F77
+   ├── #edit-managed-file-upload
+   └── <input data-drupal-selector="edit-managed-file-upload" type="file" id="edit-managed-file-upload" name="files[managed_file]" size="22" class="js-form-file form-file">
+
+ • Error: Duplicate id attribute value "edit-managed-file-multiple-upload" found on the web page.
+   ├── WCAG2AA.Principle4.Guideline4_1.4_1_1.F77
+   ├── #edit-managed-file-multiple-upload
+   └── <input data-drupal-selector="edit-managed-file-multiple-upload" multiple="multiple" type="file" id="edit-managed-file-multiple-upload" name="files[managed_file_multiple][]" size="22" class="js-form-file form-file">
+
+2 Errors
+
diff --git a/web/modules/webform/reports/accessiblity/text/example_accessibility_containers.txt b/web/modules/webform/reports/accessiblity/text/example_accessibility_containers.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0a84c6b7819f3c73c932cca34689296be4cb9140
--- /dev/null
+++ b/web/modules/webform/reports/accessiblity/text/example_accessibility_containers.txt
@@ -0,0 +1,24 @@
+
+Welcome to Pa11y
+
+ > Running Pa11y on URL http://localhost/wf/webform/example_accessibility_containers
+
+Results for URL: http://localhost/wf/webform/example_accessibility_containers
+
+ • Error: This link points to a named anchor "more" within the document, but no anchor exists with that name.
+   ├── WCAG2AA.Principle2.Guideline2_4.2_4_1.G1,G123,G124.NoSuchID
+   ├── #edit-form-element--more > div:nth-child(1) > a
+   └── <a role="button" href="#more" aria-expanded="false" aria-controls="edit-form-element--more--content">More</a>
+
+ • Error: This link points to a named anchor "more" within the document, but no anchor exists with that name.
+   ├── WCAG2AA.Principle2.Guideline2_4.2_4_1.G1,G123,G124.NoSuchID
+   ├── #edit-fieldset--more > div:nth-child(1) > a
+   └── <a role="button" href="#more" aria-expanded="false" aria-controls="edit-fieldset--more--content">More</a>
+
+ • Error: This link points to a named anchor "more" within the document, but no anchor exists with that name.
+   ├── WCAG2AA.Principle2.Guideline2_4.2_4_1.G1,G123,G124.NoSuchID
+   ├── #edit-details--more > div:nth-child(1) > a
+   └── <a role="button" href="#more" aria-expanded="false" aria-controls="edit-details--more--content">More</a>
+
+3 Errors
+
diff --git a/web/modules/webform/reports/accessiblity/text/example_accessibility_labels.txt b/web/modules/webform/reports/accessiblity/text/example_accessibility_labels.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ee8d3124fe630a7b9caf514b36e43bedc443a636
--- /dev/null
+++ b/web/modules/webform/reports/accessiblity/text/example_accessibility_labels.txt
@@ -0,0 +1,44 @@
+
+Welcome to Pa11y
+
+ > Running Pa11y on URL http://localhost/wf/webform/example_accessibility_labels
+
+Results for URL: http://localhost/wf/webform/example_accessibility_labels
+
+ • Error: This link points to a named anchor "more" within the document, but no anchor exists with that name.
+   ├── WCAG2AA.Principle2.Guideline2_4.2_4_1.G1,G123,G124.NoSuchID
+   ├── #edit-form-element--more > div:nth-child(1) > a
+   └── <a role="button" href="#more" aria-expanded="false" aria-controls="edit-form-element--more--content">More</a>
+
+ • Error: This link points to a named anchor "more" within the document, but no anchor exists with that name.
+   ├── WCAG2AA.Principle2.Guideline2_4.2_4_1.G1,G123,G124.NoSuchID
+   ├── #edit-datelist-element--more > div:nth-child(1) > a
+   └── <a role="button" href="#more" aria-expanded="false" aria-controls="edit-datelist-element--more--content">More</a>
+
+ • Error: This link points to a named anchor "more" within the document, but no anchor exists with that name.
+   ├── WCAG2AA.Principle2.Guideline2_4.2_4_1.G1,G123,G124.NoSuchID
+   ├── #edit-more--more > div:nth-child(1) > a
+   └── <a role="button" href="#more" aria-expanded="false" aria-controls="edit-more--more--content">More</a>
+
+ • Error: This link points to a named anchor "more" within the document, but no anchor exists with that name.
+   ├── WCAG2AA.Principle2.Guideline2_4.2_4_1.G1,G123,G124.NoSuchID
+   ├── #edit-fieldset--more > div:nth-child(1) > a
+   └── <a role="button" href="#more" aria-expanded="false" aria-controls="edit-fieldset--more--content">More</a>
+
+ • Error: This link points to a named anchor "more" within the document, but no anchor exists with that name.
+   ├── WCAG2AA.Principle2.Guideline2_4.2_4_1.G1,G123,G124.NoSuchID
+   ├── #edit-fieldset-more--more > div:nth-child(1) > a
+   └── <a role="button" href="#more" aria-expanded="false" aria-controls="edit-fieldset-more--more--content">More</a>
+
+ • Error: This link points to a named anchor "more" within the document, but no anchor exists with that name.
+   ├── WCAG2AA.Principle2.Guideline2_4.2_4_1.G1,G123,G124.NoSuchID
+   ├── #edit-details--more > div:nth-child(1) > a
+   └── <a role="button" href="#more" aria-expanded="false" aria-controls="edit-details--more--content">More</a>
+
+ • Error: This link points to a named anchor "more" within the document, but no anchor exists with that name.
+   ├── WCAG2AA.Principle2.Guideline2_4.2_4_1.G1,G123,G124.NoSuchID
+   ├── #edit-details-more--more > div:nth-child(1) > a
+   └── <a role="button" href="#more" aria-expanded="false" aria-controls="edit-details-more--more--content">More</a>
+
+7 Errors
+
diff --git a/web/modules/webform/reports/accessiblity/text/example_accessibility_wizard.txt b/web/modules/webform/reports/accessiblity/text/example_accessibility_wizard.txt
new file mode 100644
index 0000000000000000000000000000000000000000..87a2e95ae6fd5d220f66ce2a27bb754d0f1e5f7a
--- /dev/null
+++ b/web/modules/webform/reports/accessiblity/text/example_accessibility_wizard.txt
@@ -0,0 +1,14 @@
+
+Welcome to Pa11y
+
+ > Running Pa11y on URL http://localhost/wf/webform/example_accessibility_wizard
+
+Results for URL: http://localhost/wf/webform/example_accessibility_wizard
+
+ • Error: This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 3.12:1. Recommendation: change background to #0378d5.
+   ├── WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail
+   ├── #webform-submission-example-accessibility-wizard-add-form > div:nth-child(7) > ul > li:nth-child(1) > span:nth-child(1)
+   └── <span class="progress-marker">1</span>
+
+1 Errors
+
diff --git a/web/modules/webform/src/Access/WebformAccessResult.php b/web/modules/webform/src/Access/WebformAccessResult.php
new file mode 100644
index 0000000000000000000000000000000000000000..aaa2cfbed75923afa07068a5c3e4e079b18b2c0c
--- /dev/null
+++ b/web/modules/webform/src/Access/WebformAccessResult.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Drupal\webform\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\webform\WebformSubmissionInterface;
+
+/**
+ * Value object indicating an allowed access result, with cacheability metadata.
+ */
+class WebformAccessResult {
+
+  /**
+   * Creates an allowed or neutral access result.
+   *
+   * @param bool $condition
+   *   The condition to evaluate.
+   * @param \Drupal\Core\Entity\EntityInterface|null $webform_entity
+   *   A webform or webform submission.
+   * @param bool $cache_per_user
+   *   Cache per user.
+   *
+   * @return \Drupal\Core\Access\AccessResult
+   *   If $condition is TRUE, isAllowed() will be TRUE, otherwise isNeutral()
+   *   will be TRUE.
+   */
+  public static function allowedIf($condition, EntityInterface $webform_entity = NULL, $cache_per_user = FALSE) {
+    return $condition ? static::allowed($webform_entity, $cache_per_user) : static::neutral($webform_entity, $cache_per_user);
+  }
+
+  /**
+   * Creates an AccessResultInterface object with isAllowed() === TRUE.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|null $webform_entity
+   *   A webform or webform submission.
+   * @param bool $cache_per_user
+   *   Cache per user.
+   *
+   * @return \Drupal\Core\Access\AccessResultAllowed
+   *   isAllowed() will be TRUE.
+   */
+  public static function allowed(EntityInterface $webform_entity = NULL, $cache_per_user = FALSE) {
+    return static::addDependencies(AccessResult::allowed(), $webform_entity, $cache_per_user);
+  }
+
+  /**
+   * Creates an AccessResultInterface object with isNeutral() === TRUE.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|null $webform_entity
+   *   A webform or webform submission.
+   * @param bool $cache_per_user
+   *   Cache per user.
+   *
+   * @return \Drupal\Core\Access\AccessResultForbidden
+   *   isNeutral() will be TRUE.
+   */
+  public static function neutral(EntityInterface $webform_entity = NULL, $cache_per_user = FALSE) {
+    return static::addDependencies(AccessResult::neutral(), $webform_entity, $cache_per_user);
+  }
+
+  /**
+   * Creates an AccessResultInterface object with isForbidden() === TRUE.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|null $webform_entity
+   *   A webform or webform submission.
+   * @param bool $cache_per_user
+   *   Cache per user.
+   *
+   * @return \Drupal\Core\Access\AccessResultForbidden
+   *   isForbidden() will be TRUE.
+   */
+  public static function forbidden(EntityInterface $webform_entity = NULL, $cache_per_user = FALSE) {
+    return static::addDependencies(AccessResult::forbidden(), $webform_entity, $cache_per_user);
+  }
+
+  /**
+   * Adds dependencies to an access result.
+   *
+   * @param \Drupal\Core\Access\AccessResult $access_result
+   *   The access result.
+   * @param \Drupal\Core\Entity\EntityInterface|null $webform_entity
+   *   A webform or webform submission.
+   * @param bool $cache_per_user
+   *   Cache per user.
+   *
+   * @return \Drupal\Core\Access\AccessResult
+   *   The access result with dependencies.
+   */
+  public static function addDependencies(AccessResult $access_result, EntityInterface $webform_entity = NULL, $cache_per_user = FALSE) {
+    $access_result->cachePerPermissions();
+
+    if ($cache_per_user) {
+      $access_result->cachePerUser();
+    }
+
+    if ($webform_entity) {
+      if ($webform_entity instanceof WebformSubmissionInterface) {
+        $access_result->addCacheableDependency($webform_entity->getWebform());
+      }
+      $access_result->addCacheableDependency($webform_entity);
+    }
+
+    return $access_result;
+  }
+
+}
diff --git a/web/modules/webform/src/Access/WebformAccountAccess.php b/web/modules/webform/src/Access/WebformAccountAccess.php
index 1b91a5955abc358cfdd03bf7fdc119cc415e29b3..346102685353442a665a7754c56b4c86504873e7 100644
--- a/web/modules/webform/src/Access/WebformAccountAccess.php
+++ b/web/modules/webform/src/Access/WebformAccountAccess.php
@@ -20,7 +20,7 @@ class WebformAccountAccess {
    *   The access result.
    */
   public static function checkAdminAccess(AccountInterface $account) {
-    return AccessResult::allowedIf($account->hasPermission('administer webform') || $account->hasPermission('administer webform submission'));
+    return AccessResult::allowedIfHasPermissions($account, ['administer webform', 'administer webform submission'], 'OR');
   }
 
   /**
@@ -33,7 +33,22 @@ public static function checkAdminAccess(AccountInterface $account) {
    *   The access result.
    */
   public static function checkOverviewAccess(AccountInterface $account) {
-    return AccessResult::allowedIf($account->hasPermission('administer webform') || $account->hasPermission('administer webform submission') || $account->hasPermission('access webform overview'));
+    return AccessResult::allowedIfHasPermissions($account, ['administer webform', 'administer webform submission', 'access webform overview'], 'OR');
+  }
+
+  /**
+   * Check whether the user has 'overview' with 'create' permission.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   Run access checks for this account.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public static function checkTemplatesAccess(AccountInterface $account) {
+    $condition = ($account->hasPermission('access webform overview') &&
+      ($account->hasPermission('administer webform') || $account->hasPermission('create webform')));
+    return AccessResult::allowedIf($condition)->cachePerPermissions();
   }
 
   /**
@@ -46,7 +61,22 @@ public static function checkOverviewAccess(AccountInterface $account) {
    *   The access result.
    */
   public static function checkSubmissionAccess(AccountInterface $account) {
-    return AccessResult::allowedIf($account->hasPermission('administer webform') || $account->hasPermission('administer webform submission') || $account->hasPermission('view any webform submission'));
+    return AccessResult::allowedIfHasPermissions($account, ['administer webform', 'administer webform submission', 'view any webform submission'], 'OR');
+  }
+
+  /**
+   * Check whether the user can view own submissions.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   Run access checks for this account.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public static function checkUserSubmissionsAccess(AccountInterface $account) {
+    $condition = ($account->hasPermission('administer webform') || $account->hasPermission('administer webform submission') || $account->hasPermission('view any webform submission'))
+      || ($account->hasPermission('access webform submission user') && \Drupal::currentUser()->id() === $account->id());
+    return AccessResult::allowedIf($condition)->cachePerPermissions();
   }
 
 }
diff --git a/web/modules/webform/src/Access/WebformEntityAccess.php b/web/modules/webform/src/Access/WebformEntityAccess.php
index a755a4f4b7716aa619a341debdc37ec326e59e19..7fa5cb2735d173d164f57dc2c5e00b80291e4f50 100644
--- a/web/modules/webform/src/Access/WebformEntityAccess.php
+++ b/web/modules/webform/src/Access/WebformEntityAccess.php
@@ -11,6 +11,36 @@
  */
 class WebformEntityAccess {
 
+  /**
+   * Check whether the webform has drafts.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   * @param \Drupal\Core\Entity\EntityInterface|null $source_entity
+   *   The source entity.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public static function checkDraftsAccess(WebformInterface $webform, EntityInterface $source_entity = NULL) {
+    $draft = $webform->getSetting('draft');
+    switch ($draft) {
+      case WebformInterface::DRAFT_AUTHENTICATED:
+        $access_result = AccessResult::allowedIf(\Drupal::currentUser()->isAuthenticated());
+        break;
+
+      case WebformInterface::DRAFT_ALL:
+        $access_result = AccessResult::allowed();
+        break;
+
+      case WebformInterface::DRAFT_NONE:
+      default:
+        $access_result = AccessResult::forbidden();
+        break;
+    }
+    return $access_result->addCacheableDependency($webform);
+  }
+
   /**
    * Check whether the webform has results.
    *
@@ -71,4 +101,22 @@ public static function checkLogAccess(WebformInterface $webform = NULL, EntityIn
     return $access_result->addCacheTags(['config:webform.settings']);
   }
 
+  /**
+   * Check whether a webform setting is set to specified value.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   * @param string $setting
+   *   A webform setting.
+   * @param string $value
+   *   The setting value used to determine access.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public static function checkWebformSettingValue(WebformInterface $webform = NULL, $setting = NULL, $value = NULL) {
+    return AccessResult::allowedIf($webform->getSetting($setting) === $value)
+      ->addCacheableDependency($webform);
+  }
+
 }
diff --git a/web/modules/webform/src/Access/WebformHandlerAccess.php b/web/modules/webform/src/Access/WebformHandlerAccess.php
new file mode 100644
index 0000000000000000000000000000000000000000..1517541bcc4533c65e90ddf6c09d2d6e137deb91
--- /dev/null
+++ b/web/modules/webform/src/Access/WebformHandlerAccess.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\webform\Access;
+
+use Drupal\Core\Access\AccessResult;
+
+/**
+ * Defines the custom access control handler for the webform handlers.
+ */
+class WebformHandlerAccess {
+
+  /**
+   * Check whether the webform handler is enabled.
+   *
+   * @param string $webform_handler
+   *   A webform handler id.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public static function checkHandlerAccess($webform_handler = NULL) {
+    /** @var \Drupal\webform\Plugin\WebformHandlerManagerInterface $handler_manager */
+    $handler_manager = \Drupal::service('plugin.manager.webform.handler');
+    $handler_definitions = $handler_manager->getDefinitions();
+    $handler_definitions = $handler_manager->removeExcludeDefinitions($handler_definitions);
+    if ($webform_handler) {
+      $access_result = AccessResult::allowedIf(!empty($handler_definitions[$webform_handler]));
+    }
+    else {
+      unset($handler_definitions['broken'], $handler_definitions['email']);
+      $access_result = AccessResult::allowedIf(!empty($handler_definitions));
+    }
+    return $access_result->addCacheTags(['config:webform.settings']);
+  }
+
+}
diff --git a/web/modules/webform/src/Access/WebformSourceEntityAccess.php b/web/modules/webform/src/Access/WebformSourceEntityAccess.php
index 8f94d857ac76f7d14b062c50d15a36f25c48776b..e9a9e4d2d06955ccc08df25ed0364c1a3b4e9485 100644
--- a/web/modules/webform/src/Access/WebformSourceEntityAccess.php
+++ b/web/modules/webform/src/Access/WebformSourceEntityAccess.php
@@ -26,7 +26,9 @@ public static function checkEntityResultsAccess(EntityInterface $entity, Account
     /** @var \Drupal\webform\WebformEntityReferenceManagerInterface $entity_reference_manager */
     $entity_reference_manager = \Drupal::service('webform.entity_reference_manager');
 
-    return AccessResult::allowedIf($entity->access('update', $account) && $entity_reference_manager->getWebform($entity));
+    $access = $entity->access('update', $account, TRUE);
+    $access->andIf(AccessResult::allowedIf($entity_reference_manager->getWebform($entity)));
+    return $access;
   }
 
 }
diff --git a/web/modules/webform/src/Access/WebformSubmissionAccess.php b/web/modules/webform/src/Access/WebformSubmissionAccess.php
index c8c45ed61b7cd2b0ce78db96c26c2ef6257f85fc..e58ad9796b7103e8664558f4c8bf9db79ff071dc 100644
--- a/web/modules/webform/src/Access/WebformSubmissionAccess.php
+++ b/web/modules/webform/src/Access/WebformSubmissionAccess.php
@@ -15,13 +15,14 @@ class WebformSubmissionAccess {
    * Check whether a webform submissions' webform has wizard pages.
    *
    * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
-   *   A webform submisison.
+   *   A webform submission.
    *
    * @return \Drupal\Core\Access\AccessResultInterface
    *   The access result.
    */
   public static function checkWizardPagesAccess(WebformSubmissionInterface $webform_submission) {
-    return AccessResult::allowedIf($webform_submission->getWebform()->hasWizardPages());
+    $condition = $webform_submission->getWebform()->hasWizardPages();
+    return AccessResult::allowedIf($condition);
   }
 
   /**
@@ -36,11 +37,12 @@ public static function checkWizardPagesAccess(WebformSubmissionInterface $webfor
    *   The access result.
    */
   public static function checkResendAccess(WebformSubmissionInterface $webform_submission, AccountInterface $account) {
-    $webform = $webform_submission->getWebform();
-    if ($webform->access('submission_update_any', $account) && $webform->hasMessageHandler()) {
+    if ($webform_submission->getWebform()->hasMessageHandler()) {
       return AccessResult::allowed();
     }
-    return AccessResult::forbidden();
+    else {
+      return AccessResult::forbidden();
+    }
   }
 
 }
diff --git a/web/modules/webform/src/Ajax/WebformAnnounceCommand.php b/web/modules/webform/src/Ajax/WebformAnnounceCommand.php
new file mode 100644
index 0000000000000000000000000000000000000000..91567f02916a484196a96b692fecb86993a51645
--- /dev/null
+++ b/web/modules/webform/src/Ajax/WebformAnnounceCommand.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\webform\Ajax;
+
+use Drupal\Core\Ajax\CommandInterface;
+
+/**
+ * Provides an Ajax command to trigger audio UAs to read the supplied text.
+ *
+ * This command is implemented in Drupal.AjaxCommands.prototype.webformAnnounce.
+ */
+class WebformAnnounceCommand implements CommandInterface {
+
+  /**
+   * A string to be read by the UA.
+   *
+   * @var string
+   */
+  protected $text;
+
+  /**
+   * A string to indicate the priority of the message.
+   *
+   * Can be either 'polite' or 'assertive'.
+   *
+   * @var string
+   */
+  protected $priority;
+
+  /**
+   * Constructs a \Drupal\webform\Ajax\ScrollTopCommand object.
+   *
+   * @param string $text
+   *   A string to be read by the UA.
+   * @param string $priority
+   *   A string to indicate the priority of the message. Can be either
+   *   'polite' or 'assertive'.
+   */
+  public function __construct($text, $priority = 'polite') {
+    $this->text = $text;
+    $this->priority = $priority;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    return [
+      'command' => 'webformAnnounce',
+      'text' => $this->text,
+      'priority' => $this->priority,
+    ];
+  }
+
+}
diff --git a/web/modules/webform/src/Annotation/WebformElement.php b/web/modules/webform/src/Annotation/WebformElement.php
index bc3ee9cda2f666213007241f08d006d051ad6801..85095c9597ac81fb71d196cf65c5cce6d2aebdac 100644
--- a/web/modules/webform/src/Annotation/WebformElement.php
+++ b/web/modules/webform/src/Annotation/WebformElement.php
@@ -110,4 +110,20 @@ class WebformElement extends Plugin {
    */
   public $states_wrapper = FALSE;
 
+  /**
+   * Flag that indicates the element has been deprecated.
+   *
+   * @var bool
+   */
+  public $deprecated = FALSE;
+
+  /**
+   * Deprecated message.
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   *
+   * @ingroup plugin_translatable
+   */
+  public $deprecated_message = '';
+
 }
diff --git a/web/modules/webform/src/Annotation/WebformExporter.php b/web/modules/webform/src/Annotation/WebformExporter.php
index 08c069270722a808474c02d0835d87071d9d5851..9bc96646ba834d6fd79a79449e12708ff6f4e7e0 100644
--- a/web/modules/webform/src/Annotation/WebformExporter.php
+++ b/web/modules/webform/src/Annotation/WebformExporter.php
@@ -65,6 +65,13 @@ class WebformExporter extends Plugin {
    */
   public $archive = FALSE;
 
+  /**
+   * Download uploaded files (in a zipped archive).
+   *
+   * @var bool
+   */
+  public $files = TRUE;
+
   /**
    * Using export options.
    *
diff --git a/web/modules/webform/src/Annotation/WebformHandler.php b/web/modules/webform/src/Annotation/WebformHandler.php
index 83f0d91124fa077312af63450b61695f539e2e6d..096b95f50228d9689d23259c01f1cbcf33544b84 100644
--- a/web/modules/webform/src/Annotation/WebformHandler.php
+++ b/web/modules/webform/src/Annotation/WebformHandler.php
@@ -80,7 +80,7 @@ class WebformHandler extends Plugin {
   public $results = WebformHandlerInterface::RESULTS_IGNORED;
 
   /**
-   * Indicated whether handler support condition logic.
+   * Indicated whether handler supports condition logic.
    *
    * Most handlers will support conditional logic, this flat allows custom
    * handlers and custom modules to easily disabled conditional logic for
@@ -90,6 +90,13 @@ class WebformHandler extends Plugin {
    */
   public $conditions = TRUE;
 
+  /**
+   * Indicated whether handler supports tokens.
+   *
+   * @var bool
+   */
+  public $tokens = FALSE;
+
   /**
    * Indicated whether submission must be stored in the database for this handler processes results.
    *
diff --git a/web/modules/webform/src/Annotation/WebformSourceEntity.php b/web/modules/webform/src/Annotation/WebformSourceEntity.php
index c9d1b50816d45a4c0e0954113ee4b911b493a0f4..303bd1d8653791fe1eb26c7e643d8b7534a3d8d7 100644
--- a/web/modules/webform/src/Annotation/WebformSourceEntity.php
+++ b/web/modules/webform/src/Annotation/WebformSourceEntity.php
@@ -54,4 +54,13 @@ class WebformSourceEntity extends Plugin {
    */
   public $weight = 0;
 
+  /**
+   * The element's module dependencies.
+   *
+   * @var array
+   *
+   * @see webform_webform_element_info_alter()
+   */
+  public $dependencies = [];
+
 }
diff --git a/web/modules/webform/src/Breadcrumb/WebformBreadcrumbBuilder.php b/web/modules/webform/src/Breadcrumb/WebformBreadcrumbBuilder.php
index 9ea13646c8c0dffa39f2c33a96563155ee7b2370..250ebebaf7dca5f2103b27eacc1081f45aa76027 100644
--- a/web/modules/webform/src/Breadcrumb/WebformBreadcrumbBuilder.php
+++ b/web/modules/webform/src/Breadcrumb/WebformBreadcrumbBuilder.php
@@ -82,11 +82,17 @@ public function applies(RouteMatchInterface $route_match) {
       $path = '';
     }
 
-    if ((count($args) > 2) && $args[0] == 'entity' && ($args[2] == 'webform' ||  $args[2] == 'webform_submission')) {
+    /** @var \Drupal\webform\WebformInterface $webform */
+    $webform = ($route_match->getParameter('webform') instanceof WebformInterface) ? $route_match->getParameter('webform') : NULL;
+
+    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
+    $webform_submission = ($route_match->getParameter('webform_submission') instanceof WebformSubmissionInterface) ? $route_match->getParameter('webform_submission') : NULL;
+
+    if ((count($args) > 2) && $args[0] == 'entity' && ($args[2] == 'webform' || $args[2] == 'webform_submission')) {
       $this->type = 'webform_source_entity';
     }
-    elseif ($route_name === 'webform.element_plugins.test') {
-      $this->type = 'webform_element_plugins';
+    elseif ($route_name === 'webform.reports_plugins.elements.test') {
+      $this->type = 'webform_plugins_elements';
     }
     elseif (strpos($route_name, 'webform.contribute') === 0) {
       $this->type = 'webform_contribute';
@@ -100,21 +106,19 @@ public function applies(RouteMatchInterface $route_match) {
     elseif (strpos($route_name, 'entity.webform.handler.') === 0) {
       $this->type = 'webform_handler';
     }
-    elseif ($route_match->getParameter('webform_submission') instanceof WebformSubmissionInterface && strpos($route_name, 'webform.user.submission') !== FALSE) {
+    elseif ($webform_submission && strpos($route_name, '.webform.user.submission') !== FALSE) {
       $this->type = 'webform_user_submission';
     }
-    elseif (strpos($route_match->getRouteName(), 'webform.user.submissions') !== FALSE) {
+    elseif (strpos($route_name, '.webform.user.submissions') !== FALSE) {
       $this->type = 'webform_user_submissions';
     }
-    elseif (strpos($route_match->getRouteName(), 'webform.user.drafts') !== FALSE) {
+    elseif (strpos($route_name, '.webform.user.drafts') !== FALSE) {
       $this->type = 'webform_user_drafts';
     }
-    elseif ($route_match->getParameter('webform_submission') instanceof WebformSubmissionInterface && $route_match->getParameter('webform_submission')->access('admin')) {
+    elseif ($webform_submission && $webform_submission->access('admin')) {
       $this->type = 'webform_submission';
     }
-    elseif (($route_match->getParameter('webform') instanceof WebformInterface  && $route_match->getParameter('webform')->access('admin'))) {
-      /** @var \Drupal\webform\WebformInterface $webform */
-      $webform = $route_match->getParameter('webform');
+    elseif ($webform && $webform->access('admin')) {
       $this->type = ($webform->isTemplate() && $this->moduleHandler->moduleExists('webform_templates')) ? 'webform_template' : 'webform';
     }
     elseif (strpos($path, 'admin/structure/webform/test/') !== FALSE) {
@@ -126,7 +130,6 @@ public function applies(RouteMatchInterface $route_match) {
     else {
       $this->type = NULL;
     }
-
     return ($this->type) ? TRUE : FALSE;
   }
 
@@ -163,6 +166,13 @@ public function build(RouteMatchInterface $route_match) {
       $breadcrumb->addLink(Link::createFromRoute($this->t('Help'), 'help.main'));
       $breadcrumb->addLink(Link::createFromRoute($this->t('Webform'), 'help.page', ['name' => 'webform']));
     }
+    elseif ($this->type == 'webform_plugins_elements') {
+      $breadcrumb = new Breadcrumb();
+      $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
+      $breadcrumb->addLink(Link::createFromRoute($this->t('Administration'), 'system.admin'));
+      $breadcrumb->addLink(Link::createFromRoute($this->t('Reports'), 'system.admin_reports'));
+      $breadcrumb->addLink(Link::createFromRoute($this->t('Elements'), 'webform.reports_plugins.elements'));
+    }
     else {
       $breadcrumb = new Breadcrumb();
       $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
@@ -179,17 +189,11 @@ public function build(RouteMatchInterface $route_match) {
             $breadcrumb->addLink(Link::createFromRoute($this->t('Options'), 'entity.webform_options.collection'));
           }
           elseif (strpos($route_name, 'entity.webform_image_select_images') === 0 && $route_name !== 'entity.webform_image_select_imagess.collection') {
-            // @todo Refactor or move to webform_image_select.module.
             // @see webform_image_select.module.
             $breadcrumb->addLink(Link::createFromRoute($this->t('Images'), 'entity.webform_image_select_images.collection'));
           }
           break;
 
-        case 'webform_element_plugins':
-          $breadcrumb->addLink(Link::createFromRoute($this->t('Elements'), 'webform.element_plugins'));
-
-          break;
-
         case 'webform_test':
           $breadcrumb->addLink(Link::createFromRoute($this->t('Testing'), 'webform_test.index'));
           break;
diff --git a/web/modules/webform/src/Commands/WebformCliService.php b/web/modules/webform/src/Commands/WebformCliService.php
index 649313021e00d1ec2a8549b41de6e8326943f148..f09511664cd5ad8d3088c548c0ee0ea563ac64a0 100644
--- a/web/modules/webform/src/Commands/WebformCliService.php
+++ b/web/modules/webform/src/Commands/WebformCliService.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\webform\Commands;
 
-use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\Variable;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Mail\MailFormatHelper;
@@ -11,7 +10,9 @@
 use Drupal\webform\Entity\Webform;
 use Drupal\webform\Form\WebformResultsClearForm;
 use Drupal\webform\Form\WebformSubmissionsPurgeForm;
+use Drupal\webform\Utility\WebformObjectHelper;
 use Drupal\webform\Utility\WebformYaml;
+use Drupal\webform_submission_export_import\Form\WebformSubmissionExportImportUploadForm;
 use Drush\Commands\DrushCommands;
 use Psr\Log\LogLevel;
 
@@ -90,6 +91,7 @@ public function webform_drush_command() {
         'webform' => 'The webform ID you want to export (required unless --entity-type and --entity-id are specified)',
       ],
       'options' => [
+        'exporter' => 'The type of export. (delimited, table, yaml, or json)',
         // Delimited export options.
         'delimiter' => 'Delimiter between columns (defaults to site-wide setting). This option may need to be wrapped in quotes. i.e. --delimiter="\t".',
         'multiple-delimiter' => 'Delimiter between an element with multiple values (defaults to site-wide setting).',
@@ -102,6 +104,8 @@ public function webform_drush_command() {
         'options-multiple-format' => 'Set to "separate" (default) or "compact" to determine how multiple select list values are exported.',
         'entity-reference-items' => 'Comma-separated list of entity reference items (id, title, and/or url) to be exported.',
         'excluded-columns' => 'Comma-separated list of component IDs or webform keys to exclude.',
+        // CSV options
+        'uuid' => ' Use UUIDs for all entity references. (Only applies to CSV download)',
         // Download options.
         'entity-type' => 'The entity type to which this submission was submitted from.',
         'entity-id' => 'The ID of the entity of which this webform submission was submitted from.',
@@ -114,11 +118,30 @@ public function webform_drush_command() {
         'sticky' => 'Flagged/starred submission status.',
         'files' => 'Download files: "1" or "0" (default). If set to 1, the exported CSV file and any submission file uploads will be download in a gzipped tar file.',
         // Output options.
-        'destination' => 'The full path and filename in which the CSV or archive should be stored. If omitted the CSV file or archive will be outputted to the commandline.',
+        'destination' => 'The full path and filename in which the CSV or archive should be stored. If omitted the CSV file or archive will be outputted to the command line.',
       ],
       'aliases' => ['wfx'],
     ];
 
+    $items['webform-import'] = [
+      'description' => 'Imports webform submissions from a CSV file.',
+      'core' => ['8+'],
+      'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_SITE,
+      'arguments' => [
+        'webform' => 'The webform ID you want to import (required unless --entity-type and --entity-id are specified)',
+        'import_uri' => 'The path or URI for the CSV file to be imported.',
+      ],
+      'options' => [
+        // Import options.
+        'skip_validation' => 'Skip form validation.',
+        'treat_warnings_as_errors' => 'Treat all warnings as errors.',
+        // Source entity options.
+        'entity-type' => 'The entity type to which this submission was submitted from.',
+        'entity-id' => 'The ID of the entity of which this webform submission was submitted from.',
+      ],
+      'aliases' => ['wfi'],
+    ];
+
     $items['webform-purge'] = [
       'description' => "Purge webform submissions from the databases",
       'core' => ['8+'],
@@ -233,11 +256,11 @@ public function webform_drush_command() {
     /* Repair */
 
     $items['webform-repair'] = [
-      'description' => 'Makes sure all Webform admin settings and webforms are up-to-date.',
+      'description' => 'Makes sure all Webform admin configuration and webform settings are up-to-date.',
       'core' => ['8+'],
       'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT,
       'examples' => [
-        'webform-repair' => 'Repairs admin settings and webforms are up-to-date.',
+        'webform-repair' => 'Repairs admin configuration and webform settings are up-to-date.',
       ],
       'aliases' => ['wfr'],
     ];
@@ -348,6 +371,70 @@ public function drush_webform_export($webform_id = NULL) {
     return NULL;
   }
 
+  /******************************************************************************/
+  // Import.
+  /******************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function drush_webform_import_validate($webform_id = NULL, $import_uri = NULL) {
+    if (!\Drupal::moduleHandler()->moduleExists('webform_submission_export_import')) {
+      return $this->drush_set_error($this->dt('The Webform Submission Export/Import module must be enabled to perform imports.'));
+    }
+
+    if ($errors = $this->_drush_webform_validate($webform_id)) {
+      return $errors;
+    }
+
+    if (empty($import_uri)) {
+      return $this->drush_set_error($this->dt('Please include the CSV path or URI.'));
+    }
+    if (file_exists($import_uri)) {
+      return NULL;
+    }
+
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function drush_webform_import($webform_id = NULL, $import_uri = NULL) {
+    /** @var \Drupal\webform_submission_export_import\WebformSubmissionExportImportImporterInterface $submission_importer */
+    $submission_importer = \Drupal::service('webform_submission_export_import.importer');
+
+    // Get webform.
+    $webform = Webform::load($webform_id);
+
+    // Get source entity.
+    $entity_type = $this->drush_get_option('entity-type');
+    $entity_id = $this->drush_get_option('entity-id');
+    if ($entity_type && $entity_id) {
+      $source_entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id);
+    }
+    else {
+      $source_entity = NULL;
+    }
+
+    // Get import options
+    $import_options = $this->_drush_get_options($submission_importer->getDefaultImportOptions());
+
+    $submission_importer->setWebform($webform);
+    $submission_importer->setSourceEntity($source_entity);
+    $submission_importer->setImportOptions($import_options);
+    $submission_importer->setImportUri($import_uri);;
+    $t_args = ['@total' => $submission_importer->getTotal()];
+    if (!$this->drush_confirm($this->dt('Are you sure you want to import @total submissions?', $t_args) . PHP_EOL . $this->dt('This action cannot be undone.'))) {
+      return $this->drush_user_abort();
+    }
+
+    WebformSubmissionExportImportUploadForm::batchSet($webform, $source_entity, $import_uri, $import_options);
+    $this->drush_backend_batch_process();
+
+    return NULL;
+  }
+
   /******************************************************************************/
   // Purge
   /******************************************************************************/
@@ -394,7 +481,8 @@ public function drush_webform_purge($webform_id = NULL) {
 
     // Make sure there are submissions that need to be deleted.
     if (!$submission_storage->getTotal($webform)) {
-      return $this->drush_set_error($this->dt('There are no submissions that need to be deleted.'));
+      $this->drush_print($this->dt('There are no submissions that need to be deleted.'));
+      return;
     }
 
     if (!$webform) {
@@ -484,30 +572,44 @@ public function drush_webform_tidy($target = NULL) {
       $original_yaml = file_get_contents($file->uri);
       $tidied_yaml = $original_yaml;
 
-      // Add module dependency to exporter webform and webform options config entities.
-      if ($dependencies && preg_match('/^(webform\.webform\.|webform\.webform_options\.)/', $file->filename)) {
+      try {
+        $data = Yaml::decode($tidied_yaml);
+      }
+      catch (\Exception $exception) {
+        $message = 'Error parsing: ' . $file->filename . PHP_EOL . $exception->getMessage();
+        if (strlen($message) > 255) {
+          $message = substr($message, 0, 255) . '…';
+        }
+        $this->drush_log($message, LogLevel::ERROR);
+        $this->drush_print($message);
+        continue;
+      }
+
+      // Tidy elements.
+      if (strpos($file->filename, 'webform.webform.') === 0 && isset($data['elements'])) {
         try {
-          $data = Yaml::decode($tidied_yaml);
-          if (empty($data['dependencies']['enforced']['module']) || !in_array($target, $data['dependencies']['enforced']['module'])) {
-            $this->drush_print($this->dt('Adding module dependency to @file...', ['@file' => $file->filename]));
-            $data['dependencies']['enforced']['module'][] = $target;
-            $tidied_yaml = Yaml::encode($data);
-          }
+          $elements = WebformYaml::tidy($data['elements']);
+          $data['elements'] = $elements;
         }
         catch (\Exception $exception) {
-          $message = 'Error parsing: ' . $file->filename . PHP_EOL . $exception->getMessage();
-          if (strlen($message) > 255) {
-            $message = substr($message, 0, 255) . '...';
-          }
-          $this->drush_log($message, LogLevel::ERROR);
-          $this->drush_print($message);
+          // Do nothing.
         }
       }
 
+      // Add module dependency to exporter webform and webform options config entities.
+      if ($dependencies && preg_match('/^(webform\.webform\.|webform\.webform_options\.)/', $file->filename)) {
+        if (empty($data['dependencies']['enforced']['module']) || !in_array($target, $data['dependencies']['enforced']['module'])) {
+          $this->drush_print($this->dt('Adding module dependency to @file…', ['@file' => $file->filename]));
+          $data['dependencies']['enforced']['module'][] = $target;
+        }
+      }
+
+      $tidied_yaml = Yaml::encode($data);
+
       // Tidy and add new line to the end of the tidied file.
       $tidied_yaml = WebformYaml::tidy($tidied_yaml) . PHP_EOL;
       if ($tidied_yaml != $original_yaml) {
-        $this->drush_print($this->dt('Tidying @file...', ['@file' => $file->filename]));
+        $this->drush_print($this->dt('Tidying @file…', ['@file' => $file->filename]));
         file_put_contents($file->uri, $tidied_yaml);
         $total++;
       }
@@ -602,36 +704,26 @@ public function drush_webform_libraries_make() {
    * {@inheritdoc}
    */
   public function drush_webform_libraries_composer() {
-    // Load existing composer.json file.
-    $data = json_decode('{
-  "name": "drupal/webform",
-  "description": "Enables the creation of webforms and questionnaires.",
-  "type": "drupal-module",
-  "license": "GPL-2.0+",
-  "minimum-stability": "dev",
-  "homepage": "https://drupal.org/project/webform",
-  "authors": [
-    {
-      "name": "Jacob Rockowitz (jrockowitz)",
-      "homepage": "https://www.drupal.org/u/jrockowitz",
-      "role": "Maintainer"
-    }
-  ],
-  "support": {
-    "issues": "https://drupal.org/project/issues/webform",
-    "source": "http://cgit.drupalcode.org/webform"
-  }
-}', TRUE);
+    // Load existing composer.json file and unset certain properties.
+    $composer_path = drupal_get_path('module', 'webform') . '/composer.json';
+    $json = file_get_contents($composer_path);
+    $data = json_decode($json , FALSE, $this->drush_webform_composer_get_json_encode_options());
+    $data = (array) $data;
+    unset($data['extra'], $data['require-dev']);
+    $data = (object) $data;
 
     // Set disable tls.
     $this->drush_webform_composer_set_disable_tls($data);
 
     // Set libraries.
-    $data['repositories'] = [];
-    $data['require'] = [];
-    $this->drush_webform_composer_set_libraries($data['repositories'], $data['require']);
-
-    $this->drush_print(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
+    $data->repositories = (object) [];
+    $data->require = (object) [];
+    $this->drush_webform_composer_set_libraries($data->repositories, $data->require);
+    // Remove _webform property.
+    foreach ($data->repositories as &$repository) {
+      unset($repository['_webform']);
+    }
+    $this->drush_print(json_encode($data, $this->drush_webform_composer_get_json_encode_options()));
   }
 
   /**
@@ -640,7 +732,7 @@ public function drush_webform_libraries_composer() {
   public function drush_webform_libraries_download() {
     // Remove all existing libraries (including excluded).
     if ($this->drush_webform_libraries_remove(FALSE)) {
-      $this->drush_print($this->dt('Removing existing libraries...'));
+      $this->drush_print($this->dt('Removing existing libraries…'));
     }
 
     $temp_dir = $this->drush_tempdir();
@@ -649,6 +741,11 @@ public function drush_webform_libraries_download() {
     $libraries_manager = \Drupal::service('webform.libraries_manager');
     $libraries = $libraries_manager->getLibraries(TRUE);
     foreach ($libraries as $library_name => $library) {
+      // Skip libraries installed by other modules.
+      if (!empty($library['module'])) {
+        continue;
+      }
+
       // Download archive to temp directory.
       $download_url = $library['download_url']->toString();
       $this->drush_print("Downloading $download_url");
@@ -692,13 +789,15 @@ public function drush_webform_libraries_download() {
   public function drush_webform_libraries_remove($status = NULL) {
     $status = ($status !== FALSE);
     if ($status) {
-      $this->drush_print($this->dt('Beginning to remove libraries...'));
+      $this->drush_print($this->dt('Beginning to remove libraries…'));
     }
     $removed = FALSE;
 
     /** @var \Drupal\webform\WebformLibrariesManagerInterface $libraries_manager */
     $libraries_manager = \Drupal::service('webform.libraries_manager');
     $libraries = $libraries_manager->getLibraries();
+    // Manually add deleted libraries, so that they will always be removed.
+    $libraries['jquery.word-and-character-counter'] = 'jquery.word-and-character-counter';
     foreach ($libraries as $library_name => $library) {
       $library_path = '/libraries/' . $library_name;
       $library_exists = (file_exists(DRUPAL_ROOT . $library_path)) ? TRUE : FALSE;
@@ -710,7 +809,7 @@ public function drush_webform_libraries_remove($status = NULL) {
             '@name' => $library_name,
             '@path' => $library_path,
           ];
-          $this->drush_print($this->dt('@name removed from @path...', $t_args));
+          $this->drush_print($this->dt('@name removed from @path…', $t_args));
         }
       }
     }
@@ -727,6 +826,8 @@ public function drush_webform_libraries_remove($status = NULL) {
 
   /**
    * {@inheritdoc}
+   *
+   * @see \Drupal\webform\Form\AdminConfig\WebformAdminConfigAdvancedForm::submitForm
    */
   public function drush_webform_repair() {
     if (!$this->drush_confirm($this->dt("Are you sure you want repair the Webform module's admin settings and webforms?"))) {
@@ -735,23 +836,26 @@ public function drush_webform_repair() {
 
     module_load_include('install', 'webform');
 
-    $this->drush_print('Repairing admin settings...');
+    $this->drush_print($this->dt('Repairing webform submission storage schema…'));
+    _webform_update_webform_submission_storage_schema();
+
+    $this->drush_print($this->dt('Repairing admin settings…'));
     _webform_update_admin_settings(TRUE);
 
-    $this->drush_print('Repairing webform settings...');
+    $this->drush_print($this->dt('Repairing webform settings…'));
     _webform_update_webform_settings();
 
-    $this->drush_print('Repairing webform handlers...');
-    _webform_update_webform_handler_configuration();
+    $this->drush_print($this->dt('Repairing webform handlers…'));
+    _webform_update_webform_handler_settings();
 
-    $this->drush_print('Repairing webform field storage definitions...');
+    $this->drush_print($this->dt('Repairing webform field storage definitions…'));
     _webform_update_field_storage_definitions();
 
-    $this->drush_print('Repairing webform submission storage schema...');
+    $this->drush_print($this->dt('Repairing webform submission storage schema…'));
     _webform_update_webform_submission_storage_schema();
 
     // Validate all webform elements.
-    $this->drush_print('Validating webform elements...');
+    $this->drush_print($this->dt('Validating webform elements…'));
     /** @var \Drupal\webform\WebformEntityElementsValidatorInterface $elements_validator */
     $elements_validator = \Drupal::service('webform.elements_validator');
 
@@ -759,7 +863,7 @@ public function drush_webform_repair() {
     $webforms = Webform::loadMultiple();
     foreach ($webforms as $webform) {
       if ($messages = $elements_validator->validate($webform)) {
-        $this->drush_print('  ' . t('@title (@id): Found element validation errors.', ['@title' => $webform->label(), '@id' => $webform->id()]));
+        $this->drush_print('  ' . $this->dt('@title (@id): Found element validation errors.', ['@title' => $webform->label(), '@id' => $webform->id()]));
         foreach ($messages as $message) {
           $this->drush_print('  - ' . strip_tags($message));
         }
@@ -865,7 +969,7 @@ protected function _drush_webform_docs_tidy($html) {
     // Configuration.
     // - http://us3.php.net/manual/en/book.tidy.php
     // - http://tidy.sourceforge.net/docs/quickref.html#wrap
-    $config = ['show-body-only' => TRUE, 'wrap' => '0'];
+    $config = ['show-body-only' => TRUE, 'wrap' => '10000'];
 
     $tidy = new \tidy();
     $tidy->parseString($html, $config, 'utf8');
@@ -879,6 +983,9 @@ protected function _drush_webform_docs_tidy($html) {
     $html = preg_replace('#<pre><code>\s*#', "<code>\n", $html);
     $html = preg_replace('#\s*</code></pre>#', "\n</code>", $html);
 
+    // Fix code in webform-libraries.html.
+    $html = str_replace(' &gt; ', ' > ', $html);
+
     // Remove space after <br> tags.
     $html = preg_replace('/(<br[^>]*>)\s+/', '\1', $html);
 
@@ -936,25 +1043,29 @@ public function drush_webform_composer_update() {
     $composer_directory = $this->composer_directory;
 
     $json = file_get_contents($composer_json);
-    $data = Json::decode($json) + [
-      'repositories' => [],
-      'require' => [],
-    ];
+    $data = json_decode($json, FALSE, $this->drush_webform_composer_get_json_encode_options());
+    if (!isset($data->repositories)) {
+      $data->repositories = (object) [];
+    }
+    if (!isset($data->require)) {
+      $data->repositories = (object) [];
+    }
 
     // Add drupal-library to installer paths.
     if (strpos($json, 'type:drupal-library') === FALSE) {
-      $data['extra']['installer-paths'][$composer_directory . 'libraries/{$name}'][] = 'type:drupal-library';
+      $library_path = $composer_directory . 'libraries/{$name}';
+      $data->extra->{'installer-paths'}->{$library_path}[] = 'type:drupal-library';
     }
 
     // Get repositories and require.
-    $repositories = &$data['repositories'];
-    $require = &$data['require'];
+    $repositories = &$data->repositories;
+    $require = &$data->require;
 
     // Remove all existing _webform repositories.
     foreach ($repositories as $repository_name => $repository) {
-      if (!empty($repository['_webform'])) {
-        $package_name = $repositories[$repository_name]['package']['name'];
-        unset($repositories[$repository_name], $require[$package_name]);
+      if (!empty($repository->_webform)) {
+        $package_name = $repositories->{$repository_name}->package->name;
+        unset($repositories->{$repository_name}, $require->{$package_name});
       }
     }
 
@@ -964,55 +1075,78 @@ public function drush_webform_composer_update() {
     // Set libraries.
     $this->drush_webform_composer_set_libraries($repositories, $require);
 
-    $json_options = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES;
-    file_put_contents($composer_json, json_encode($data, $json_options));
+    file_put_contents($composer_json, json_encode($data, $this->drush_webform_composer_get_json_encode_options()));
 
     $this->drush_print("$composer_json updated.");
-    $this->drush_print('Make sure to run `composer update`.');
+    $this->drush_print('Make sure to run `composer update --lock`.');
   }
 
+  /**
+   * Get Composer specific JSON encode options.
+   *
+   * @return int
+   *   Composer specific JSON encode options.
+   *
+   * @see https://getcomposer.org/apidoc/1.6.2/Composer/Json/JsonFile.html#method_encode
+   */
+  protected function drush_webform_composer_get_json_encode_options() {
+    return JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE;
+  }
 
   /**
    * Set composer disable tls.
    *
    * This is needed when CKEditor's HTTPS server's SSL is not working properly.
    *
-   * @param array $data
+   * @param object $data
    *   Composer JSON data.
    */
-  protected function drush_webform_composer_set_disable_tls(array &$data) {
+  protected function drush_webform_composer_set_disable_tls(&$data) {
     // Remove disable-tls config.
-    if (isset($data['config']) && isset($data['config']['disable-tls'])) {
-      unset($data['config']['disable-tls']);
+    if (isset($data->config) && isset($data->config->{'disable-tls'})) {
+      unset($data->config->{'disable-tls'});
     }
     if ($this->drush_get_option('disable-tls')) {
-      $data['config']['disable-tls'] = TRUE;
+      $data->config->{'disable-tls'} = TRUE;
     }
   }
 
   /**
    * Set composer libraries.
    *
-   * @param array $repositories
+   * @param object $repositories
    *   Composer repositories.
-   * @param array $require
+   * @param object $require
    *   Composer require.
    */
-  protected function drush_webform_composer_set_libraries(array &$repositories, array &$require) {
+  protected function drush_webform_composer_set_libraries(&$repositories, &$require) {
     /** @var \Drupal\webform\WebformLibrariesManagerInterface $libraries_manager */
     $libraries_manager = \Drupal::service('webform.libraries_manager');
     $libraries = $libraries_manager->getLibraries(TRUE);
     foreach ($libraries as $library_name => $library) {
       // Never overwrite existing repositories.
-      if (isset($repositories[$library_name])) {
+      if (isset($repositories->{$library_name})) {
+        continue;
+      }
+
+      // Skip libraries installed by other modules.
+      if (!empty($library['module'])) {
         continue;
       }
 
       $dist_url = $library['download_url']->toString();
-      $dist_type = (preg_match('/\.zip$/', $dist_url)) ? 'zip' : 'file';
+      if (preg_match('/\.zip$/', $dist_url)) {
+        $dist_type = 'zip';
+      }
+      elseif (preg_match('/\.tgz$/', $dist_url)) {
+        $dist_type = 'tar';
+      }
+      else {
+        $dist_type = 'file';
+      }
       $package_version = $library['version'];
       $package_name = (strpos($library_name, '.') === FALSE) ? "$library_name/$library_name" : str_replace('.', '/', $library_name);
-      $repositories[$library_name] = [
+      $repositories->$library_name = [
         '_webform' => TRUE,
         'type' => 'package',
         'package' => [
@@ -1032,10 +1166,10 @@ protected function drush_webform_composer_set_libraries(array &$repositories, ar
         ],
       ];
 
-      $require[$package_name] = $package_version;
+      $require->$package_name = $package_version;
     }
-    ksort($repositories);
-    ksort($require);
+    $repositories = WebformObjectHelper::sortByProperty($repositories);
+    $require = WebformObjectHelper::sortByProperty($require);
   }
 
   /******************************************************************************/
@@ -1079,8 +1213,8 @@ protected function drush_webform_generate_commands_drush8() {
 /******************************************************************************/";
 
       // Validate.
-      $validate_method = 'drush_' . str_replace('-','_', $command_key) . '_validate';
-      $validate_hook = 'drush_' . str_replace('-','_', $command_key) . '_validate';
+      $validate_method = 'drush_' . str_replace('-', '_', $command_key) . '_validate';
+      $validate_hook = 'drush_' . str_replace('-', '_', $command_key) . '_validate';
       if (method_exists($this, $validate_method)) {
         $functions[] = "
 /**
@@ -1092,8 +1226,8 @@ function $validate_hook() {
       }
 
       // Commands.
-      $command_method = 'drush_' . str_replace('-','_', $command_key);
-      $command_hook = 'drush_' . str_replace('-','_', $command_key);
+      $command_method = 'drush_' . str_replace('-', '_', $command_key);
+      $command_hook = 'drush_' . str_replace('-', '_', $command_key);
       if (method_exists($this, $command_method)) {
         $functions[] = "
 /**
@@ -1105,7 +1239,7 @@ function $command_hook() {
       }
     }
 
-    // Build commands
+    // Build commands.
     $commands = Variable::export($this->webform_drush_command());
     // Remove [datatypes] which are only needed for Drush 9.x.
     $commands = preg_replace('/\[(boolean)\]\s+/', '', $commands);
@@ -1148,7 +1282,7 @@ protected function drush_webform_generate_commands_drush9() {
 
     $methods = [];
     foreach ($items as $command_key => $command_item) {
-      $command_name = str_replace('-',':', $command_key);
+      $command_name = str_replace('-', ':', $command_key);
 
       // Set defaults.
       $command_item += [
@@ -1165,7 +1299,7 @@ protected function drush_webform_generate_commands_drush9() {
   /****************************************************************************/";
 
       // Validate.
-      $validate_method = 'drush_' . str_replace('-','_', $command_key) . '_validate';
+      $validate_method = 'drush_' . str_replace('-', '_', $command_key) . '_validate';
       if (method_exists($this, $validate_method)) {
         $methods[] = "
   /**
@@ -1179,7 +1313,7 @@ public function $validate_method(CommandData \$commandData) {
       }
 
       // Command.
-      $command_method = 'drush_' . str_replace('-','_', $command_key);
+      $command_method = 'drush_' . str_replace('-', '_', $command_key);
       if (method_exists($this, $command_method)) {
         $command_params = [];
         $command_arguments = [];
@@ -1208,7 +1342,7 @@ public function $validate_method(CommandData \$commandData) {
           }
 
           $command_annotations[] = "@option $option_name $option_description";
-          $command_options[$option_name] = $option_default ;
+          $command_options[$option_name] = $option_default;
         }
         if ($command_options) {
           $command_options = Variable::export($command_options);
@@ -1220,6 +1354,7 @@ public function $validate_method(CommandData \$commandData) {
 
         // usage.
         foreach ($command_item['examples'] as $example_name => $example_description) {
+          $example_name = str_replace('-', ':', $example_name);
           $command_annotations[] = "@usage $example_name";
           $command_annotations[] = "  $example_description";
         }
@@ -1318,16 +1453,40 @@ protected function _drush_webform_validate($webform_id = NULL) {
     return NULL;
   }
 
+  /**
+   * Get drush command options with dashed converted to underscores.
+   *
+   * @param array $default_options
+   *   The commands default options
+   *
+   * @return array
+   *   An associative array of options.
+   */
+  protected function _drush_get_options(array $default_options) {
+    $options = $this->drush_redispatch_get_options();
+    // Convert dashes to underscores.
+    foreach ($options as $key => $value) {
+      unset($options[$key]);
+      if (isset($default_options[$key]) && is_array($default_options[$key])) {
+        $value = explode(',', $value);
+      }
+      $options[str_replace('-', '_', $key)] = $value;
+    }
+    $options += $default_options;
+    return $options;
+  }
+
 }
 
 // Add dt() function so that the WebformEditorialController can extract
 // editorial.
 // @see \Drupal\webform_editorial\Controller\WebformEditorialController::drush
 if (!function_exists('dt')) {
+
   /**
    * Rudimentary replacement for Drupal API t() function.
    *
-   * @param string
+   * @param string $string
    *   String to process, possibly with replacement item.
    *
    * @return string
@@ -1336,5 +1495,5 @@ protected function _drush_webform_validate($webform_id = NULL) {
   function dt($string) {
     return $string;
   }
-}
 
+}
diff --git a/web/modules/webform/src/Commands/WebformCommands.php b/web/modules/webform/src/Commands/WebformCommands.php
index 9d94030bca718ff0e72e375fa4a0b540fb4edc53..afab32a5a35f2e59759be264a1ff7f1852fc4689 100644
--- a/web/modules/webform/src/Commands/WebformCommands.php
+++ b/web/modules/webform/src/Commands/WebformCommands.php
@@ -34,6 +34,7 @@ public function drush_webform_export_validate(CommandData $commandData) {
    *
    * @command webform:export
    * @param $webform The webform ID you want to export (required unless --entity-type and --entity-id are specified)
+   * @option exporter The type of export. (delimited, table, yaml, or json)
    * @option delimiter Delimiter between columns (defaults to site-wide setting). This option may need to be wrapped in quotes. i.e. --delimiter="\t".
    * @option multiple-delimiter Delimiter between an element with multiple values (defaults to site-wide setting).
    * @option file-name File name used to export submission and uploaded filed. You may use tokens.
@@ -43,6 +44,7 @@ public function drush_webform_export_validate(CommandData $commandData) {
    * @option options-multiple-format Set to "separate" (default) or "compact" to determine how multiple select list values are exported.
    * @option entity-reference-items Comma-separated list of entity reference items (id, title, and/or url) to be exported.
    * @option excluded-columns Comma-separated list of component IDs or webform keys to exclude.
+   * @option uuid  Use UUIDs for all entity references. (Only applies to CSV download)
    * @option entity-type The entity type to which this submission was submitted from.
    * @option entity-id The ID of the entity of which this webform submission was submitted from.
    * @option range-type Range of submissions to export: "all", "latest", "serial", "sid", or "date".
@@ -53,13 +55,42 @@ public function drush_webform_export_validate(CommandData $commandData) {
    * @option state Submission state to be included: "completed", "draft" or "all" (default).
    * @option sticky Flagged/starred submission status.
    * @option files Download files: "1" or "0" (default). If set to 1, the exported CSV file and any submission file uploads will be download in a gzipped tar file.
-   * @option destination The full path and filename in which the CSV or archive should be stored. If omitted the CSV file or archive will be outputted to the commandline.
+   * @option destination The full path and filename in which the CSV or archive should be stored. If omitted the CSV file or archive will be outputted to the command line.
    * @aliases wfx
    */
-  public function drush_webform_export($webform = NULL, array $options = ['delimiter' => NULL, 'multiple-delimiter' => NULL, 'file-name' => NULL, 'header-format' => NULL, 'options-item-format' => NULL, 'options-single-format' => NULL, 'options-multiple-format' => NULL, 'entity-reference-items' => NULL, 'excluded-columns' => NULL, 'entity-type' => NULL, 'entity-id' => NULL, 'range-type' => NULL, 'range-latest' => NULL, 'range-start' => NULL, 'range-end' => NULL, 'order' => NULL, 'state' => NULL, 'sticky' => NULL, 'files' => NULL, 'destination' => NULL]) {
+  public function drush_webform_export($webform = NULL, array $options = ['exporter' => NULL, 'delimiter' => NULL, 'multiple-delimiter' => NULL, 'file-name' => NULL, 'header-format' => NULL, 'options-item-format' => NULL, 'options-single-format' => NULL, 'options-multiple-format' => NULL, 'entity-reference-items' => NULL, 'excluded-columns' => NULL, 'uuid' => NULL, 'entity-type' => NULL, 'entity-id' => NULL, 'range-type' => NULL, 'range-latest' => NULL, 'range-start' => NULL, 'range-end' => NULL, 'order' => NULL, 'state' => NULL, 'sticky' => NULL, 'files' => NULL, 'destination' => NULL]) {
     $this->cliService->drush_webform_export($webform);
   }
 
+  /****************************************************************************/
+  // drush webform:import. DO NOT EDIT.
+  /****************************************************************************/
+
+  /**
+   * @hook validate webform:import
+   */
+  public function drush_webform_import_validate(CommandData $commandData) {
+    $arguments = $commandData->arguments();
+    array_shift($arguments);
+    call_user_func_array([$this->cliService, 'drush_webform_import_validate'], $arguments);
+  }
+
+  /**
+   * Imports webform submissions from a CSV file.
+   *
+   * @command webform:import
+   * @param $webform The webform ID you want to import (required unless --entity-type and --entity-id are specified)
+   * @param $import_uri The path or URI for the CSV file to be imported.
+   * @option skip_validation Skip form validation.
+   * @option treat_warnings_as_errors Treat all warnings as errors.
+   * @option entity-type The entity type to which this submission was submitted from.
+   * @option entity-id The ID of the entity of which this webform submission was submitted from.
+   * @aliases wfi
+   */
+  public function drush_webform_import($webform = NULL, $import_uri = NULL, array $options = ['skip_validation' => NULL, 'treat_warnings_as_errors' => NULL, 'entity-type' => NULL, 'entity-id' => NULL]) {
+    $this->cliService->drush_webform_import($webform, $import_uri);
+  }
+
   /****************************************************************************/
   // drush webform:purge. DO NOT EDIT.
   /****************************************************************************/
@@ -81,11 +112,11 @@ public function drush_webform_purge_validate(CommandData $commandData) {
    * @option all Flush all submissions
    * @option entity-type The entity type for webform submissions to be purged
    * @option entity-id The ID of the entity for webform submissions to be purged
-   * @usage drush webform-purge
+   * @usage drush webform:purge
    *   Pick a webform and then purge its submissions.
-   * @usage drush webform-purge contact
+   * @usage drush webform:purge contact
    *   Delete 'Contact' webform submissions.
-   * @usage drush webform-purge --all
+   * @usage drush webform:purge ::all
    *   Purge all webform submissions.
    * @aliases wfp
    */
@@ -113,7 +144,7 @@ public function drush_webform_tidy_validate(CommandData $commandData) {
    * @param $target The module (config/install), config directory (sync), or path (/some/path) that needs its YAML configuration files tidied. (Defaults to webform)
    * @option dependencies Add module dependencies to installed webform and options configuration entities.
    * @option prefix Prefix for file names to be tidied. (Defaults to webform)
-   * @usage drush webform-tidy webform
+   * @usage drush webform:tidy webform
    *   Tidies YAML configuration files in 'webform/config' for the Webform module
    * @aliases wft
    */
@@ -129,7 +160,7 @@ public function drush_webform_tidy($target = NULL, array $options = ['dependenci
    * Displays the status of third party libraries required by the Webform module.
    *
    * @command webform:libraries:status
-   * @usage webform-libraries-status
+   * @usage webform:libraries:status
    *   Displays the status of third party libraries required by the Webform module.
    * @aliases wfls
    */
@@ -145,7 +176,7 @@ public function drush_webform_libraries_status() {
    * Generates libraries YAML to be included in a drush.make.yml files.
    *
    * @command webform:libraries:make
-   * @usage webform-libraries-make
+   * @usage webform:libraries:make
    *   Generates libraries YAML to be included in a drush.make.yml file.
    * @aliases wflm
    */
@@ -162,7 +193,7 @@ public function drush_webform_libraries_make() {
    *
    * @command webform:libraries:composer
    * @option disable-tls If set to true all HTTPS URLs will be tried with HTTP instead and no network level encryption is performed.
-   * @usage webform-libraries-composer
+   * @usage webform:libraries:composer
    *   Generates the Webform module's composer.json with libraries as repositories.
    * @aliases wflc
    */
@@ -178,7 +209,7 @@ public function drush_webform_libraries_composer(array $options = ['disable-tls'
    * Download third party libraries required by the Webform module.
    *
    * @command webform:libraries:download
-   * @usage webform-libraries-download
+   * @usage webform:libraries:download
    *   Download third party libraries required by the Webform module.
    * @aliases wfld
    */
@@ -194,7 +225,7 @@ public function drush_webform_libraries_download() {
    * Removes all downloaded third party libraries required by the Webform module.
    *
    * @command webform:libraries:remove
-   * @usage webform-libraries-remove
+   * @usage webform:libraries:remove
    *   Removes all downloaded third party libraries required by the Webform module.
    * @aliases wflr
    */
@@ -236,11 +267,11 @@ public function drush_webform_generate($webform_id = NULL, $num = NULL, array $o
   /****************************************************************************/
 
   /**
-   * Makes sure all Webform admin settings and webforms are up-to-date.
+   * Makes sure all Webform admin configuration and webform settings are up-to-date.
    *
    * @command webform:repair
-   * @usage webform-repair
-   *   Repairs admin settings and webforms are up-to-date.
+   * @usage webform:repair
+   *   Repairs admin configuration and webform settings are up-to-date.
    * @aliases wfr
    */
   public function drush_webform_repair() {
@@ -264,7 +295,7 @@ public function drush_webform_docs_validate(CommandData $commandData) {
    * Generates HTML documentation.
    *
    * @command webform:docs
-   * @usage webform-repair
+   * @usage webform:repair
    *   Generates HTML documentation used by the Webform module's documentation pages.
    * @aliases wfd
    */
@@ -290,7 +321,7 @@ public function drush_webform_composer_update_validate(CommandData $commandData)
    *
    * @command webform:composer:update
    * @option disable-tls If set to true all HTTPS URLs will be tried with HTTP instead and no network level encryption is performed.
-   * @usage webform-composer-update
+   * @usage webform:composer:update
    *   Updates the Drupal installation's composer.json to include the Webform module's selected libraries as repositories.
    * @aliases wfcu
    */
@@ -306,7 +337,7 @@ public function drush_webform_composer_update(array $options = ['disable-tls' =>
    * Generate Drush commands from webform.drush.inc for Drush 8.x to WebformCommands for Drush 9.x.
    *
    * @command webform:generate:commands
-   * @usage drush webform-generate-commands
+   * @usage drush webform:generate:commands
    *   Generate Drush commands from webform.drush.inc for Drush 8.x to WebformCommands for Drush 9.x.
    * @aliases wfgc
    */
diff --git a/web/modules/webform/src/Commands/WebformCommandsBase.php b/web/modules/webform/src/Commands/WebformCommandsBase.php
index b7f28b4179a08aa2f95dc81dfcf1b060faf4320c..bbbc623eaa1e1fa42caae4fb7502832e52e2f373 100644
--- a/web/modules/webform/src/Commands/WebformCommandsBase.php
+++ b/web/modules/webform/src/Commands/WebformCommandsBase.php
@@ -110,9 +110,17 @@ public function drush_mkdir($path) {
 
   public function drush_tarball_extract($path, $destination = FALSE) {
     $this->drush_mkdir($destination);
-    $return = drush_shell_cd_and_exec(dirname($path), "unzip %s -d %s", $path, $destination);
-    if (!$return) {
-      throw new \Exception(dt('Unable to unzip !filename.', ['!filename' => $path]));
+    if (preg_match('/\.tgz$/', $path)) {
+      $return = drush_shell_cd_and_exec(dirname($path), "tar -xvzf %s -C %s", $path, $destination);
+      if (!$return) {
+        throw new \Exception(dt('Unable to extract !filename.' . PHP_EOL . implode(PHP_EOL, drush_shell_exec_output()), ['!filename' => $path]));
+      }
+    }
+    else {
+      $return = drush_shell_cd_and_exec(dirname($path), "unzip %s -d %s", $path, $destination);
+      if (!$return) {
+        throw new \Exception(dt('Unable to extract !filename.' . PHP_EOL . implode(PHP_EOL, drush_shell_exec_output()), ['!filename' => $path]));
+      }
     }
     return $return;
   }
diff --git a/web/modules/webform/src/Controller/WebformAddonsController.php b/web/modules/webform/src/Controller/WebformAddonsController.php
index e1771d8791c0e3dbb21a7ac7548ab32cf6af97ae..c6eb16ab107906b3c76fb61ac7f4ef3df76925b8 100644
--- a/web/modules/webform/src/Controller/WebformAddonsController.php
+++ b/web/modules/webform/src/Controller/WebformAddonsController.php
@@ -7,12 +7,20 @@
 use Drupal\webform\Element\WebformMessage;
 use Drupal\webform\WebformAddonsManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Provides route responses for webform add-on.
  */
 class WebformAddonsController extends ControllerBase implements ContainerInjectionInterface {
 
+  /**
+   * The current request.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
   /**
    * The webform add-ons manager.
    *
@@ -23,10 +31,13 @@ class WebformAddonsController extends ControllerBase implements ContainerInjecti
   /**
    * Constructs a WebformAddonsController object.
    *
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
    * @param \Drupal\webform\WebformAddonsManagerInterface $addons
    *   The webform add-ons manager.
    */
-  public function __construct(WebformAddonsManagerInterface $addons) {
+  public function __construct(RequestStack $request_stack, WebformAddonsManagerInterface $addons) {
+    $this->request = $request_stack->getCurrentRequest();
     $this->addons = $addons;
   }
 
@@ -35,6 +46,7 @@ public function __construct(WebformAddonsManagerInterface $addons) {
    */
   public static function create(ContainerInterface $container) {
     return new static(
+      $container->get('request_stack'),
       $container->get('webform.addons_manager')
     );
   }
@@ -53,6 +65,34 @@ public function index() {
       ],
     ];
 
+    // Filter.
+    $build['filter'] = [
+      '#type' => 'search',
+      '#title' => $this->t('Filter'),
+      '#title_display' => 'invisible',
+      '#size' => 30,
+      '#placeholder' => $this->t('Filter by keyword'),
+      '#attributes' => [
+        'class' => ['webform-form-filter-text'],
+        'data-summary' => '.webform-addons-summary',
+        'data-item-singlular' => $this->t('add-on'),
+        'data-item-plural' => $this->t('add-ons'),
+        'data-no-results' => '.webform-addons-no-results',
+        'data-element' => '.admin-list',
+        'data-source' => 'li',
+        'data-parent' => 'li',
+        'title' => $this->t('Enter a keyword to filter by.'),
+        'autofocus' => 'autofocus',
+      ],
+    ];
+
+    // Display info.
+    $build['info'] = [
+      '#markup' => $this->t('@total add-ons', ['@total' => count($this->addons->getProjects())]),
+      '#prefix' => '<p class="webform-addons-summary">',
+      '#suffix' => '</p>',
+    ];
+
     // Projects.
     $build['projects'] = [
       '#type' => 'container',
@@ -60,7 +100,11 @@ public function index() {
         'class' => ['webform-addons-projects', 'js-webform-details-toggle', 'webform-details-toggle'],
       ],
     ];
-    $build['projects']['#attached']['library'][] = 'webform/webform.addons';
+
+    // Store and disable compact mode.
+    // @see system_admin_compact_mode
+    $system_admin_compact_mode = system_admin_compact_mode();
+    $this->request->cookies->set('Drupal_visitor_admin_compact_mode', FALSE);
 
     $categories = $this->addons->getCategories();
     foreach ($categories as $category_name => $category) {
@@ -88,7 +132,8 @@ public function index() {
               '#message_type' => 'warning',
               '#message_close' => TRUE,
               '#message_storage' => WebformMessage::STORAGE_USER,
-              '#message_message' => $this->t('Please install to the <a href=":href">@title</a> project to improve the Webform module\'s user experience.', [':href' => $project['url']->toString(), '@title' => $project['title']]),
+              '#message_message' => $this->t('Please install to the <a href=":href">@title</a> project to improve the Webform module\'s user experience.', [':href' => $project['url']->toString(), '@title' => $project['title']]) .
+                ' <em>' . $project['install'] . '</em>',
               '#weight' => -100,
             ];
           }
@@ -101,6 +146,20 @@ public function index() {
       ];
     }
 
+    // Reset compact mode to stored setting.
+    $this->request->cookies->get('Drupal_visitor_admin_compact_mode', $system_admin_compact_mode);
+
+    // No results.
+    $build['no_results'] = [
+      '#type' => 'webform_message',
+      '#message_message' => $this->t('No add-ons found. Try a different search.'),
+      '#message_type' => 'info',
+      '#attributes' => ['class' => ['webform-addons-no-results']],
+    ];
+
+    $build['#attached']['library'][] = 'webform/webform.addons';
+    $build['#attached']['library'][] = 'webform/webform.admin';
+
     return $build;
   }
 
diff --git a/web/modules/webform/src/Controller/WebformContributeController.php b/web/modules/webform/src/Controller/WebformContributeController.php
index a6cf6bfd960a2c8ced7d139e4d22c7b815230f6d..67813ca08a57bb6d3b11ae3f412b8f7c87b98a2f 100644
--- a/web/modules/webform/src/Controller/WebformContributeController.php
+++ b/web/modules/webform/src/Controller/WebformContributeController.php
@@ -5,8 +5,8 @@
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Url;
+use Drupal\webform\WebformContributeManagerInterface;
 use GuzzleHttp\ClientInterface;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
@@ -18,30 +18,30 @@
 class WebformContributeController extends ControllerBase implements ContainerInjectionInterface {
 
   /**
-   * The configuration object factory.
+   * The Guzzle HTTP client.
    *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   * @var \GuzzleHttp\ClientInterface
    */
-  protected $configFactory;
+  protected $httpClient;
 
   /**
    * The Guzzle HTTP client.
    *
-   * @var \GuzzleHttp\ClientInterface
+   * @var \Drupal\webform\WebformContributeManagerInterface
    */
-  protected $httpClient;
+  protected $contributeManager;
 
   /**
-   * Constructs a WebfomrContributeController object.
+   * Constructs a WebformContributeController object.
    *
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The configuration object factory.
    * @param \GuzzleHttp\ClientInterface $http_client
    *   The Guzzle HTTP client.
+   * @param \Drupal\webform\WebformContributeManagerInterface $contribute_manager
+   *   The webform contribute manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ClientInterface $http_client) {
-    $this->configFactory = $config_factory;
+  public function __construct(ClientInterface $http_client, WebformContributeManagerInterface $contribute_manager) {
     $this->httpClient = $http_client;
+    $this->contributeManager = $contribute_manager;
   }
 
   /**
@@ -49,8 +49,8 @@ public function __construct(ConfigFactoryInterface $config_factory, ClientInterf
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('config.factory'),
-      $container->get('http_client')
+      $container->get('http_client'),
+      $container->get('webform.contribute_manager')
     );
   }
 
@@ -64,9 +64,6 @@ public static function create(ContainerInterface $container) {
    *   A renderable array containing a webform about page.
    */
   public function index(Request $request) {
-    /** @var \Drupal\webform\WebformContributeManagerInterface $contribute_manager */
-    $contribute_manager = \Drupal::service('webform.contribute_manager');
-
     // Message.
     $build['message'] = [];
     $build['message']['divide'] = $this->buildDivider();
@@ -79,9 +76,9 @@ public function index(Request $request) {
     // Community Information.
     $build['community_info'] = [
       '#theme' => 'webform_contribute',
-      '#account' => $contribute_manager->getAccount(),
-      '#membership' => $contribute_manager->getMembership(),
-      '#contribution' => $contribute_manager->getContribution(),
+      '#account' => $this->contributeManager->getAccount(),
+      '#membership' => $this->contributeManager->getMembership(),
+      '#contribution' => $this->contributeManager->getContribution(),
     ];
 
     // Drupal.
@@ -211,7 +208,6 @@ public function autocomplete(Request $request, $account_type = 'user') {
     }
   }
 
-
   /****************************************************************************/
   // Build methods.
   /****************************************************************************/
@@ -263,7 +259,7 @@ protected function buildLink($title, $url, array $class = ['button']) {
    *   A video player, linked button, or an empty array if videos are disabled.
    */
   protected function buildVideo($youtube_id) {
-    $video_display = $this->configFactory->get('webform.settings')->get('ui.video_display');
+    $video_display = $this->config('webform.settings')->get('ui.video_display');
     switch ($video_display) {
       case 'dialog':
         return [
diff --git a/web/modules/webform/src/Controller/WebformElementController.php b/web/modules/webform/src/Controller/WebformElementController.php
index 6efed57f9de744f109dff414c8f8584361943d92..5a92435f77f51e0a95cf251cb819155b380a740d 100644
--- a/web/modules/webform/src/Controller/WebformElementController.php
+++ b/web/modules/webform/src/Controller/WebformElementController.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\webform\Controller;
 
-use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Database\Database;
@@ -77,7 +76,7 @@ public function autocomplete(Request $request, WebformInterface $webform, $key)
     ];
 
     // Check minimum number of characters.
-    if (Unicode::strlen($q) < (int) $element['#autocomplete_match']) {
+    if (mb_strlen($q) < (int) $element['#autocomplete_match']) {
       return new JsonResponse([]);
     }
 
diff --git a/web/modules/webform/src/Controller/WebformEntityController.php b/web/modules/webform/src/Controller/WebformEntityController.php
index b4733ccd4dccdd30cb812f84bc30be2247d1b30f..efe87b3df37d76a344a9ef0ea5e8354bd73fb57b 100644
--- a/web/modules/webform/src/Controller/WebformEntityController.php
+++ b/web/modules/webform/src/Controller/WebformEntityController.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Render\RendererInterface;
+use Drupal\webform\Element\Webform as WebformElement;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformRequestInterface;
 use Drupal\webform\WebformSubmissionInterface;
@@ -183,11 +184,13 @@ public function confirmation(Request $request, WebformInterface $webform = NULL,
    *   The current request.
    * @param bool $templates
    *   If TRUE, limit autocomplete matches to webform templates.
+   * @param bool $archived
+   *   If TRUE, limit autocomplete matches to archived webforms and templates.
    *
    * @return \Symfony\Component\HttpFoundation\JsonResponse
    *   The JSON response.
    */
-  public function autocomplete(Request $request, $templates = FALSE) {
+  public function autocomplete(Request $request, $templates = FALSE, $archived = FALSE) {
     $q = $request->query->get('q');
 
     $webform_storage = $this->entityTypeManager()->getStorage('webform');
@@ -210,6 +213,9 @@ public function autocomplete(Request $request, $templates = FALSE) {
       $query->condition('template', FALSE);
     }
 
+    // Limit query to archived.
+    $query->condition('archive', $archived);
+
     $entity_ids = $query->execute();
 
     if (empty($entity_ids)) {
@@ -228,6 +234,32 @@ public function autocomplete(Request $request, $templates = FALSE) {
     return new JsonResponse($matches);
   }
 
+  /**
+   * Returns a webform's access denied page.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   The webform.
+   *
+   * @return array
+   *   A renderable array containing an access denied page.
+   */
+  public function accessDenied(WebformInterface $webform) {
+    return WebformElement::buildAccessDenied($webform);
+  }
+
+  /**
+   * Returns a webform's access denied title.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   The webform.
+   *
+   * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
+   *   The webform submissions's access denied title.
+   */
+  public function accessDeniedTitle(WebformInterface $webform) {
+    return $webform->getSetting('form_access_denied_title') ?: $this->t('Access denied');
+  }
+
   /**
    * Route title callback.
    *
@@ -245,7 +277,42 @@ public function title(WebformInterface $webform = NULL) {
     else {
       $source_entity = $this->requestHandler->getCurrentSourceEntity('webform');
     }
-    return ($source_entity) ? $source_entity->label() : $webform->label();
+
+    // If source entity does not exist or does not have a label always use
+    // the webform's label.
+    if (!$source_entity || !method_exists($source_entity, 'label')) {
+      return $webform->label();
+    }
+
+    // Handler duplicate titles.
+    if ($source_entity->label() === $webform->label()) {
+      return $webform->label();
+    }
+
+    // Get the webform's title.
+    switch ($webform->getSetting('form_title')) {
+      case WebformInterface::TITLE_SOURCE_ENTITY:
+        return $source_entity->label();
+
+      case WebformInterface::TITLE_WEBFORM:
+        return $webform->label();
+
+      case WebformInterface::TITLE_WEBFORM_SOURCE_ENTITY:
+        $t_args = [
+          '@source_entity' => $source_entity->label(),
+          '@webform' => $webform->label(),
+        ];
+        return $this->t('@webform: @source_entity', $t_args);
+
+      case WebformInterface::TITLE_SOURCE_ENTITY_WEBFORM:
+      default:
+        $t_args = [
+          '@source_entity' => $source_entity->label(),
+          '@webform' => $webform->label(),
+        ];
+        return $this->t('@source_entity: @webform', $t_args);
+
+    }
   }
 
 }
diff --git a/web/modules/webform/src/Controller/WebformHelpController.php b/web/modules/webform/src/Controller/WebformHelpController.php
new file mode 100644
index 0000000000000000000000000000000000000000..92b9110da7bdbe3a01f58e1a1fdb9ee034b91888
--- /dev/null
+++ b/web/modules/webform/src/Controller/WebformHelpController.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\webform\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\webform\WebformHelpManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides route responses for webform help.
+ */
+class WebformHelpController extends ControllerBase implements ContainerInjectionInterface {
+
+  /**
+   * The webform help manager.
+   *
+   * @var \Drupal\webform\WebformHelpManagerInterface
+   */
+  protected $help;
+
+  /**
+   * Constructs a WebformHelpController object.
+   *
+   * @param \Drupal\webform\WebformHelpManagerInterface $help
+   *   The webform help manager.
+   */
+  public function __construct(WebformHelpManagerInterface $help) {
+    $this->help = $help;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('webform.help_manager')
+    );
+  }
+
+  /**
+   * Returns the Webform extend page.
+   *
+   * @return array
+   *   The webform submission webform.
+   */
+  public function index() {
+    return $this->help->buildIndex();
+  }
+
+}
diff --git a/web/modules/webform/src/Controller/WebformOptionsController.php b/web/modules/webform/src/Controller/WebformOptionsController.php
index 01a5b7b10043af08007ce5154f649d874bbd6653..ed02d24094568bb04c783750c885417c4e5742fb 100644
--- a/web/modules/webform/src/Controller/WebformOptionsController.php
+++ b/web/modules/webform/src/Controller/WebformOptionsController.php
@@ -2,80 +2,54 @@
 
 namespace Drupal\webform\Controller;
 
+use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Controller\ControllerBase;
-use Drupal\webform\Entity\WebformOptions;
-use Drupal\webform\WebformInterface;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
- * Provides route responses for webform options.
+ * Provides route responses for webform options options.
  */
 class WebformOptionsController extends ControllerBase {
 
   /**
-   * Returns response for the element autocompletion.
+   * Returns response for the webform options autocompletion.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
    *   The current request object containing the search string.
-   * @param \Drupal\webform\WebformInterface $webform
-   *   A webform.
-   * @param string $key
-   *   Webform element key.
    *
    * @return \Symfony\Component\HttpFoundation\JsonResponse
    *   A JSON response containing the autocomplete suggestions.
    */
-  public function autocomplete(Request $request, WebformInterface $webform, $key) {
+  public function autocomplete(Request $request) {
     $q = $request->query->get('q');
 
-    // Make sure the current user can access this webform.
-    if (!$webform->access('view')) {
-      return new JsonResponse([]);
-    }
+    $webform_options_storage = $this->entityTypeManager()->getStorage('webform_options');
 
-    // Get the webform element element.
-    $elements = $webform->getElementsInitializedAndFlattened();
-    if (!isset($elements[$key])) {
-      return new JsonResponse([]);
-    }
+    $query = $webform_options_storage->getQuery()
+      ->range(0, 10)
+      ->sort('label');
 
-    // Get the element's webform options.
-    $element = $elements[$key];
-    $element['#options'] = $element['#autocomplete'];
-    $options = WebformOptions::getElementOptions($element);
-    if (empty($options)) {
+    // Query title and id.
+    $or = $query->orConditionGroup()
+      ->condition('id', $q, 'CONTAINS')
+      ->condition('label', $q, 'CONTAINS');
+    $query->condition($or);
+
+    $entity_ids = $query->execute();
+
+    if (empty($entity_ids)) {
       return new JsonResponse([]);
     }
+    $webform_options = $webform_options_storage->loadMultiple($entity_ids);
 
-    // Filter and convert options to autocomplete matches.
     $matches = [];
-    $this->appendOptionsToMatchesRecursive($q, $options, $matches);
-    return new JsonResponse($matches);
-  }
-
-  /**
-   * Append webform options to autocomplete matches.
-   *
-   * @param string $q
-   *   String to filter option's label by.
-   * @param array $options
-   *   An associative array of webform options.
-   * @param array $matches
-   *   An associative array of autocomplete matches.
-   */
-  protected function appendOptionsToMatchesRecursive($q, array $options, array &$matches) {
-    foreach ($options as $value => $label) {
-      if (is_array($label)) {
-        $this->appendOptionsToMatchesRecursive($q, $label, $matches);
-      }
-      elseif (stripos($label, $q) !== FALSE) {
-        $matches[] = [
-          'value' => $value,
-          'label' => $label,
-        ];
-      }
+    foreach ($webform_options as $webform_option) {
+      $value = new FormattableMarkup('@label (@id)', ['@label' => $webform_option->label(), '@id' => $webform_option->id()]);
+      $matches[] = ['value' => $value, 'label' => $value];
     }
+
+    return new JsonResponse($matches);
   }
 
 }
diff --git a/web/modules/webform/src/Controller/WebformPluginElementController.php b/web/modules/webform/src/Controller/WebformPluginElementController.php
index a2160a8393a0c3f2011d1e53ff9639cbaafaba2d..375511c1df2343894e73d158bed4173a63b4b24d 100644
--- a/web/modules/webform/src/Controller/WebformPluginElementController.php
+++ b/web/modules/webform/src/Controller/WebformPluginElementController.php
@@ -3,7 +3,6 @@
 namespace Drupal\webform\Controller;
 
 use Drupal\Component\Render\FormattableMarkup;
-use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Controller\ControllerBase;
@@ -18,13 +17,6 @@
  */
 class WebformPluginElementController extends ControllerBase implements ContainerInjectionInterface {
 
-  /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
   /**
    * The module handler.
    *
@@ -49,8 +41,6 @@ class WebformPluginElementController extends ControllerBase implements Container
   /**
    * Constructs a WebformPluginElementController object.
    *
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The factory for configuration objects.
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler.
    * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
@@ -58,8 +48,7 @@ class WebformPluginElementController extends ControllerBase implements Container
    * @param \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager
    *   A webform element plugin manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, ElementInfoManagerInterface $element_info, WebformElementManagerInterface $element_manager) {
-    $this->configFactory = $config_factory;
+  public function __construct(ModuleHandlerInterface $module_handler, ElementInfoManagerInterface $element_info, WebformElementManagerInterface $element_manager) {
     $this->moduleHandler = $module_handler;
     $this->elementInfo = $element_info;
     $this->elementManager = $element_manager;
@@ -70,7 +59,6 @@ public function __construct(ConfigFactoryInterface $config_factory, ModuleHandle
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('config.factory'),
       $container->get('module_handler'),
       $container->get('plugin.manager.element_info'),
       $container->get('plugin.manager.webform.element')
@@ -144,7 +132,7 @@ public function index() {
             'data' => [
               '#type' => 'link',
               '#title' => $element_plugin_id,
-              '#url' => new Url('webform.element_plugins.test', ['type' => $element_plugin_id]),
+              '#url' => new Url('webform.reports_plugins.elements.test', ['type' => $element_plugin_id]),
               '#attributes' => ['class' => ['webform-form-filter-text-source']],
             ],
           ];
@@ -154,7 +142,19 @@ public function index() {
         }
 
         // Description.
-        $description = new FormattableMarkup('<strong>@label</strong><br />@description', ['@label' => $webform_element->getPluginLabel(), '@description' => $webform_element->getPluginDescription()]);
+        $description = [
+          'data' => [
+            'title_description' => ['#markup' => new FormattableMarkup('<strong>@label</strong><br />@description', ['@label' => $webform_element->getPluginLabel(), '@description' => $webform_element->getPluginDescription()])],
+          ],
+        ];
+        // Add deprecated warning.
+        if (!empty($webform_element_plugin_definition['deprecated'])) {
+          $description['data']['deprecated'] = [
+            '#type' => 'webform_message',
+            '#message_message' => $webform_element_plugin_definition['deprecated_message'],
+            '#message_type' => 'warning',
+          ];
+        }
 
         // Parent classes.
         $parent_classes = WebformReflectionHelper::getParentClasses($webform_element, 'WebformElementBase');
@@ -180,17 +180,19 @@ public function index() {
           'container' => $webform_element->isContainer($element),
           'root' => $webform_element->isRoot(),
           'hidden' => $webform_element->isHidden(),
+          'composite' => $webform_element->isComposite(),
           'multiple' => $webform_element->supportsMultipleValues(),
           'multiline' => $webform_element->isMultiline($element),
           'default_key' => $webform_element_plugin_definition['default_key'],
           'states_wrapper' => $webform_element_plugin_definition['states_wrapper'],
+          'deprecated' => $webform_element_plugin_definition['deprecated'],
         ];
         $webform_info = [];
         foreach ($webform_info_definitions as $key => $value) {
           $webform_info[] = '<b>' . $key . '</b>: ' . ($value ? $this->t('Yes') : $this->t('No'));
         }
 
-        // Wlement info.
+        // Element info.
         $element_info_definitions = [
           'input' => (empty($webform_element_info['#input'])) ? $this->t('No') : $this->t('Yes'),
           'theme' => (isset($webform_element_info['#theme'])) ? $webform_element_info['#theme'] : 'N/A',
@@ -215,7 +217,7 @@ public function index() {
         }
         $properties += $element_default_properties;
         if (count($properties) >= 20) {
-          $properties = array_slice($properties, 0, 20) + ['...' => '...'];
+          $properties = array_slice($properties, 0, 20) + ['…' => '…'];
         }
 
         // Operations.
@@ -223,7 +225,7 @@ public function index() {
         if ($test_element_enabled) {
           $operations['test'] = [
             'title' => $this->t('Test'),
-            'url' => new Url('webform.element_plugins.test', ['type' => $element_plugin_id]),
+            'url' => new Url('webform.reports_plugins.elements.test', ['type' => $element_plugin_id]),
           ];
         }
         if ($api_url = $webform_element->getPluginApiUrl()) {
@@ -246,12 +248,14 @@ public function index() {
             $dependencies ? ['data' => ['#markup' => '• ' . implode('<br />• ', $dependencies)], 'nowrap' => 'nowrap'] : '',
             $element_plugin_definition['provider'],
             $webform_element_plugin_definition['provider'],
-            $operations ? ['data' => [
-              '#type' => 'operations',
-              '#links' => $operations,
-              '#prefix' => '<div class="webform-dropbutton">',
-              '#suffix' => '</div>',
-            ]] : '',
+            $operations ? [
+              'data' => [
+                '#type' => 'operations',
+                '#links' => $operations,
+                '#prefix' => '<div class="webform-dropbutton">',
+                '#suffix' => '</div>',
+              ],
+            ] : '',
           ],
         ];
         if (isset($excluded_elements[$element_plugin_id])) {
@@ -276,7 +280,10 @@ public function index() {
       '#placeholder' => $this->t('Filter by element name'),
       '#attributes' => [
         'class' => ['webform-form-filter-text'],
-        'data-element' => '.webform-element-plugin',
+        'data-element' => '.webform-element-plugin-table',
+        'data-summary' => '.webform-element-plugin-summary',
+        'data-item-singlular' => $this->t('element'),
+        'data-item-plural' => $this->t('elements'),
         'title' => $this->t('Enter a part of the element type to filter by.'),
         'autofocus' => 'autofocus',
       ],
@@ -293,7 +300,7 @@ public function index() {
     // Display info.
     $build['info'] = [
       '#markup' => $this->t('@total elements', ['@total' => count($webform_form_element_rows)]),
-      '#prefix' => '<p>',
+      '#prefix' => '<p class="webform-element-plugin-summary">',
       '#suffix' => '</p>',
     ];
 
@@ -315,8 +322,9 @@ public function index() {
         $this->t('Operations'),
       ],
       '#rows' => $webform_form_element_rows,
+      '#sticky' => TRUE,
       '#attributes' => [
-        'class' => ['webform-element-plugin'],
+        'class' => ['webform-element-plugin-table'],
       ],
     ];
 
diff --git a/web/modules/webform/src/Controller/WebformPluginExporterController.php b/web/modules/webform/src/Controller/WebformPluginExporterController.php
index 6ba7953990ebe73268a5078498b33338ffabb8a4..f6920a1122dc16958a4400cf4722e6a3e7cf50bd 100644
--- a/web/modules/webform/src/Controller/WebformPluginExporterController.php
+++ b/web/modules/webform/src/Controller/WebformPluginExporterController.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\webform\Controller;
 
-use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Component\Plugin\PluginManagerInterface;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
@@ -14,13 +13,6 @@
  */
 class WebformPluginExporterController extends ControllerBase implements ContainerInjectionInterface {
 
-  /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
   /**
    * A results exporter plugin manager.
    *
@@ -31,13 +23,10 @@ class WebformPluginExporterController extends ControllerBase implements Containe
   /**
    * Constructs a WebformPluginExporterController object.
    *
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The factory for configuration objects.
    * @param \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager
    *   A results exporter plugin manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, PluginManagerInterface $plugin_manager) {
-    $this->configFactory = $config_factory;
+  public function __construct(PluginManagerInterface $plugin_manager) {
     $this->pluginManager = $plugin_manager;
   }
 
@@ -46,7 +35,6 @@ public function __construct(ConfigFactoryInterface $config_factory, PluginManage
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('config.factory'),
       $container->get('plugin.manager.webform.exporter')
     );
   }
diff --git a/web/modules/webform/src/Controller/WebformPluginHandlerController.php b/web/modules/webform/src/Controller/WebformPluginHandlerController.php
index fb0dc6583a8245b4f3ce10d4867062aafc5847cd..df999058a021530f99a557b5512e900f279467cc 100644
--- a/web/modules/webform/src/Controller/WebformPluginHandlerController.php
+++ b/web/modules/webform/src/Controller/WebformPluginHandlerController.php
@@ -3,7 +3,6 @@
 namespace Drupal\webform\Controller;
 
 use Drupal\Component\Plugin\PluginManagerInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Url;
@@ -18,13 +17,6 @@
  */
 class WebformPluginHandlerController extends ControllerBase implements ContainerInjectionInterface {
 
-  /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
   /**
    * A webform handler plugin manager.
    *
@@ -33,15 +25,12 @@ class WebformPluginHandlerController extends ControllerBase implements Container
   protected $pluginManager;
 
   /**
-   * Constructs a WebformPluginHanderController object.
+   * Constructs a WebformPluginHandlerController object.
    *
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The factory for configuration objects.
    * @param \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager
    *   A webform handler plugin manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, PluginManagerInterface $plugin_manager) {
-    $this->configFactory = $config_factory;
+  public function __construct(PluginManagerInterface $plugin_manager) {
     $this->pluginManager = $plugin_manager;
   }
 
@@ -50,7 +39,6 @@ public function __construct(ConfigFactoryInterface $config_factory, PluginManage
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('config.factory'),
       $container->get('plugin.manager.webform.handler')
     );
   }
@@ -88,7 +76,7 @@ public function index() {
 
     $build = [];
 
-    // Settings
+    // Settings.
     $build['settings'] = [
       '#type' => 'link',
       '#title' => $this->t('Edit configuration'),
@@ -231,6 +219,8 @@ public function listHandlers(Request $request, WebformInterface $webform) {
       '#attributes' => [
         'class' => ['webform-form-filter-text'],
         'data-element' => '.webform-handler-add-table',
+        'data-item-singlular' => $this->t('handler'),
+        'data-item-plural' => $this->t('handlers'),
         'title' => $this->t('Enter a part of the handler name to filter by.'),
         'autofocus' => 'autofocus',
       ],
@@ -240,6 +230,7 @@ public function listHandlers(Request $request, WebformInterface $webform) {
       '#type' => 'table',
       '#header' => $headers,
       '#rows' => $rows,
+      '#sticky' => TRUE,
       '#empty' => $this->t('No handler available.'),
       '#attributes' => [
         'class' => ['webform-handler-add-table'],
diff --git a/web/modules/webform/src/Controller/WebformResultsExportController.php b/web/modules/webform/src/Controller/WebformResultsExportController.php
index e062c82168d0abb2b68f52bceb421ebb81080361..4fbdfb6e22504af9721bb31c90ceba9e6040f2b2 100644
--- a/web/modules/webform/src/Controller/WebformResultsExportController.php
+++ b/web/modules/webform/src/Controller/WebformResultsExportController.php
@@ -95,7 +95,7 @@ public function index(Request $request) {
         $route_name = $this->requestHandler->getRouteName($webform, $source_entity, 'webform.results_export_file');
         $route_parameters = $this->requestHandler->getRouteParameters($webform, $source_entity) + ['filename' => $query['filename']];
         $file_url = Url::fromRoute($route_name, $route_parameters, ['absolute' => TRUE])->toString();
-        drupal_set_message($this->t('Export creation complete. Your download should begin now. If it does not start, <a href=":href">download the file here</a>. This file may only be downloaded once.', [':href' => $file_url]));
+        $this->messenger()->addStatus($this->t('Export creation complete. Your download should begin now. If it does not start, <a href=":href">download the file here</a>. This file may only be downloaded once.', [':href' => $file_url]));
         $build['#attached']['html_head'][] = [
           [
             '#tag' => 'meta',
@@ -110,7 +110,7 @@ public function index(Request $request) {
 
       return $build;
     }
-    elseif ($query && empty($query['ajax_form'])) {
+    elseif ($query && empty($query['ajax_form']) && isset($query['download'])) {
       $default_options = $this->submissionExporter->getDefaultExportOptions();
       foreach ($query as $key => $value) {
         if (isset($default_options[$key]) && is_array($default_options[$key]) && is_string($value)) {
@@ -181,7 +181,15 @@ public function file(Request $request, $filename) {
    *   A response object containing the CSV file.
    */
   public function downloadFile($file_path, $download = TRUE) {
-    $response = new BinaryFileResponse($file_path, 200, [], FALSE, $download ? 'attachment' : 'inline');
+    $headers = [];
+
+    // If the file is not meant to be downloaded, allow CSV files to be
+    // displayed as plain text.
+    if (!$download && preg_match('/\.csv$/', $file_path)) {
+      $headers['Content-Type'] = 'text/plain';
+    }
+
+    $response = new BinaryFileResponse($file_path, 200, $headers, FALSE, $download ? 'attachment' : 'inline');
     $response->deleteFileAfterSend(TRUE);
     return $response;
   }
@@ -255,7 +263,7 @@ public static function batchProcess(WebformInterface $webform, EntityInterface $
 
     if (empty($context['sandbox'])) {
       $context['sandbox']['progress'] = 0;
-      $context['sandbox']['current_sid'] = 0;
+      $context['sandbox']['offset'] = 0;
       $context['sandbox']['max'] = $submission_exporter->getQuery()->count()->execute();
       // Store entity ids and not the actual webform or source entity in the
       // $context to prevent "The container was serialized" errors.
@@ -269,17 +277,16 @@ public static function batchProcess(WebformInterface $webform, EntityInterface $
 
     // Write CSV records.
     $query = $submission_exporter->getQuery();
-    $query->condition('sid', $context['sandbox']['current_sid'], '>');
-    $query->range(0, $submission_exporter->getBatchLimit());
+    $query->range($context['sandbox']['offset'], $submission_exporter->getBatchLimit());
     $entity_ids = $query->execute();
     $webform_submissions = WebformSubmission::loadMultiple($entity_ids);
     $submission_exporter->writeRecords($webform_submissions);
 
     // Track progress.
     $context['sandbox']['progress'] += count($webform_submissions);
-    $context['sandbox']['current_sid'] = ($webform_submissions) ? end($webform_submissions)->id() : 0;
+    $context['sandbox']['offset'] += $submission_exporter->getBatchLimit();
 
-    $context['message'] = t('Exported @count of @total submissions...', ['@count' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']]);
+    $context['message'] = t('Exported @count of @total submissions…', ['@count' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']]);
 
     // Track finished.
     if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
@@ -323,7 +330,7 @@ public static function batchFinish($success, array $results, array $operations)
       @unlink($file_path);
       $archive_path = $submission_exporter->getArchiveFilePath();
       @unlink($archive_path);
-      drupal_set_message(t('Finished with an error.'));
+      \Drupal::messenger()->addStatus(t('Finished with an error.'));
     }
     else {
       $submission_exporter->writeFooter();
diff --git a/web/modules/webform/src/Controller/WebformSubmissionController.php b/web/modules/webform/src/Controller/WebformSubmissionController.php
index 80f733bdc0d5ab8fe3fedacd9f3e13a5fbb9bb96..431109d16604474147095fd3bbfd4f1ea592167f 100644
--- a/web/modules/webform/src/Controller/WebformSubmissionController.php
+++ b/web/modules/webform/src/Controller/WebformSubmissionController.php
@@ -5,15 +5,69 @@
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\HtmlCommand;
-use Drupal\Core\Ajax\InvokeCommand;
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\webform\Ajax\WebformAnnounceCommand;
+use Drupal\webform\Element\WebformHtmlEditor;
+use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformSubmissionInterface;
+use Drupal\webform\WebformRequestInterface;
+use Drupal\webform\WebformTokenManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides route responses for webform submissions.
  */
 class WebformSubmissionController extends ControllerBase {
 
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Webform request handler.
+   *
+   * @var \Drupal\webform\WebformRequestInterface
+   */
+  protected $requestHandler;
+
+  /**
+   * The webform token manager.
+   *
+   * @var \Drupal\webform\WebformTokenManagerInterface
+   */
+  protected $tokenManager;
+
+  /**
+   * Constructs a WebformSubmissionController object.
+   *
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   * @param \Drupal\webform\WebformRequestInterface $request_handler
+   *   The webform request handler.
+   * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
+   *   The webform token manager.
+   */
+  public function __construct(RendererInterface $renderer, WebformRequestInterface $request_handler, WebformTokenManagerInterface $token_manager) {
+    $this->renderer = $renderer;
+    $this->requestHandler = $request_handler;
+    $this->tokenManager = $token_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('renderer'),
+      $container->get('webform.request'),
+      $container->get('webform.token_manager')
+    );
+  }
+
   /**
    * Toggle webform submission sticky.
    *
@@ -27,14 +81,19 @@ public function sticky(WebformSubmissionInterface $webform_submission) {
     // Toggle sticky.
     $webform_submission->setSticky(!$webform_submission->isSticky())->save();
 
-    // Get state.
-    $state = $webform_submission->isSticky() ? 'on' : 'off';
+    // Get selector.
+    $selector = '#webform-submission-' . $webform_submission->id() . '-sticky';
 
     $response = new AjaxResponse();
-    $response->addCommand(new HtmlCommand(
-      '#webform-submission-' . $webform_submission->id() . '-sticky',
-      new FormattableMarkup('<span class="webform-icon webform-icon-sticky webform-icon-sticky--@state"></span>', ['@state' => $state])
-    ));
+
+    // Update sticky.
+    $response->addCommand(new HtmlCommand($selector, static::buildSticky($webform_submission)));
+
+    // Announce sticky status.
+    $t_args = ['@label' => $webform_submission->label()];
+    $text = $webform_submission->isSticky() ? $this->t('@label flagged/starred.', $t_args) : $this->t('@label unflagged/unstarred.', $t_args);
+    $response->addCommand(new WebformAnnounceCommand($text));
+
     return $response;
   }
 
@@ -51,16 +110,104 @@ public function locked(WebformSubmissionInterface $webform_submission) {
     // Toggle locked.
     $webform_submission->setLocked(!$webform_submission->isLocked())->save();
 
-    // Get state.
-    $state = $webform_submission->isLocked() ? 'on' : 'off';
-
     // Get selector.
     $selector = '#webform-submission-' . $webform_submission->id() . '-locked';
 
     $response = new AjaxResponse();
-    $response->addCommand(new HtmlCommand($selector, new FormattableMarkup('<span class="webform-icon webform-icon-lock webform-icon-locked--@state"></span>', ['@state' => $state])));
-    $response->addCommand(new InvokeCommand($selector, 'trigger', ['blur']));
+
+    // Update lock.
+    $response->addCommand(new HtmlCommand($selector, static::buildLocked($webform_submission)));
+
+    // Announce lock status.
+    $t_args = ['@label' => $webform_submission->label()];
+    $text = $webform_submission->isLocked() ? $this->t('@label locked.', $t_args) : $this->t('@label unlocked.', $t_args);
+    $response->addCommand(new WebformAnnounceCommand($text));
     return $response;
   }
 
+  /**
+   * Build sticky icon.
+   *
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   *
+   * @return \Drupal\Component\Render\FormattableMarkup
+   *   Sticky icon.
+   */
+  public static function buildSticky(WebformSubmissionInterface $webform_submission) {
+    $t_args = ['@label' => $webform_submission->label()];
+    $args = [
+      '@state' => $webform_submission->isSticky() ? 'on' : 'off',
+      '@label' => $webform_submission->isSticky() ? t('Unstar/Unflag @label', $t_args) : t('Star/flag @label', $t_args),
+    ];
+    return new FormattableMarkup('<span class="webform-icon webform-icon-sticky webform-icon-sticky--@state"></span><span class="visually-hidden">@label</span>', $args);
+  }
+
+  /**
+   * Build locked icon.
+   *
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   *
+   * @return \Drupal\Component\Render\FormattableMarkup
+   *   Locked icon.
+   */
+  public static function buildLocked(WebformSubmissionInterface $webform_submission) {
+    $t_args = ['@label' => $webform_submission->label()];
+    $args = [
+      '@state' => $webform_submission->isLocked() ? 'on' : 'off',
+      '@label' => $webform_submission->isLocked() ? t('Unlock @label', $t_args) : t('Lock @label', $t_args),
+    ];
+    return new FormattableMarkup('<span class="webform-icon webform-icon-lock webform-icon-locked--@state"></span><span class="visually-hidden">@label</span>', $args);
+  }
+
+  /**
+   * Returns a webform submissions's access denied page.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   The webform.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   *
+   * @return array
+   *   A renderable array containing an access denied page.
+   */
+  public function accessDenied(WebformInterface $webform, WebformSubmissionInterface $webform_submission) {
+    // Message.
+    $config = $this->config('webform.settings');
+    $message = $webform->getSetting('submission_access_denied_message')
+      ?: $config->get('settings.default_submission_access_denied_message');
+    $message = $this->tokenManager->replace($message, $webform_submission);
+
+    // Attributes.
+    $attributes = $webform->getSetting('submission_access_denied_attributes');
+    $attributes['class'][] = 'webform-submission-access-denied';
+
+    // Build message.
+    $build = [
+      '#type' => 'container',
+      '#attributes' => $attributes,
+      'message' => WebformHtmlEditor::checkMarkup($message),
+    ];
+
+    // Add config and webform to cache contexts.
+    $this->renderer->addCacheableDependency($build, $config);
+    $this->renderer->addCacheableDependency($build, $webform);
+
+    return $build;
+  }
+
+  /**
+   * Returns a webform 's access denied title.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   The webform.
+   *
+   * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
+   *   The webform's access denied title.
+   */
+  public function accessDeniedTitle(WebformInterface $webform) {
+    return $webform->getSetting('submission_access_denied_title') ?: $this->t('Access denied');
+  }
+
 }
diff --git a/web/modules/webform/src/Controller/WebformSubmissionViewController.php b/web/modules/webform/src/Controller/WebformSubmissionViewController.php
index 7120296e9b62febafe69313a8aa8a37bc99c5214..baeaf69e948cf8777e239162b3afc742beafd683 100644
--- a/web/modules/webform/src/Controller/WebformSubmissionViewController.php
+++ b/web/modules/webform/src/Controller/WebformSubmissionViewController.php
@@ -30,7 +30,7 @@ class WebformSubmissionViewController extends EntityViewController {
    *
    * @var \Drupal\webform\WebformRequestInterface
    */
-  protected $requestManager;
+  protected $requestHandler;
 
   /**
    * Creates an WebformSubmissionViewController object.
@@ -47,7 +47,7 @@ class WebformSubmissionViewController extends EntityViewController {
   public function __construct(EntityManagerInterface $entity_manager, RendererInterface $renderer, AccountInterface $current_user, WebformRequestInterface $webform_request) {
     parent::__construct($entity_manager, $renderer);
     $this->currentUser = $current_user;
-    $this->requestManager = $webform_request;
+    $this->requestHandler = $webform_request;
   }
 
   /**
@@ -66,18 +66,25 @@ public static function create(ContainerInterface $container) {
    * {@inheritdoc}
    */
   public function view(EntityInterface $webform_submission, $view_mode = 'default', $langcode = NULL) {
-    $webform = $this->requestManager->getCurrentWebform();
-    $source_entity = $this->requestManager->getCurrentSourceEntity('webform_submission');
+    $webform = $this->requestHandler->getCurrentWebform();
+    $source_entity = $this->requestHandler->getCurrentSourceEntity('webform_submission');
+
+    // Set webform submission template.
+    $build = [
+      '#theme' => 'webform_submission',
+      '#view_mode' => $view_mode,
+      '#webform_submission' => $webform_submission,
+    ];
 
     // Navigation.
     $build['navigation'] = [
-      '#theme' => 'webform_submission_navigation',
+      '#type' => 'webform_submission_navigation',
       '#webform_submission' => $webform_submission,
     ];
 
     // Information.
     $build['information'] = [
-      '#theme' => 'webform_submission_information',
+      '#type' => 'webform_submission_information',
       '#webform_submission' => $webform_submission,
       '#source_entity' => $source_entity,
     ];
diff --git a/web/modules/webform/src/Controller/WebformSubmissionsController.php b/web/modules/webform/src/Controller/WebformSubmissionsController.php
new file mode 100644
index 0000000000000000000000000000000000000000..8ca03579cd4cdf1b80208079a966901a6ca9a233
--- /dev/null
+++ b/web/modules/webform/src/Controller/WebformSubmissionsController.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\webform\Controller;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\webform\WebformInterface;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides route responses for webform submissions.
+ */
+class WebformSubmissionsController extends ControllerBase {
+
+  /**
+   * Returns response for the source entity autocompletion.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request object containing the search string.
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   *
+   * @return \Symfony\Component\HttpFoundation\JsonResponse
+   *   A JSON response containing the autocomplete suggestions.
+   */
+  public function sourceEntityAutocomplete(Request $request, WebformInterface $webform) {
+    $match = $request->query->get('q');
+
+    $webform_submission_storage = $this->entityTypeManager()->getStorage('webform_submission');
+    $source_entities = $webform_submission_storage->getSourceEntities($webform);
+    $matches = [];
+
+    // @see \Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection::buildEntityQuery
+    foreach ($source_entities as $source_entity_type => $source_entity_ids) {
+      $definition = $this->entityTypeManager()->getDefinition($source_entity_type);
+      $storage = $this->entityTypeManager()->getStorage($source_entity_type);
+
+      if (empty($definition->getKey('id')) || empty($definition->getKey('label'))) {
+        continue;
+      }
+
+      $query = $storage->getQuery();
+      $query->range(0, 10);
+      $query->condition($definition->getKey('id'), $source_entity_ids, 'IN');
+      $query->condition($query->orConditionGroup()
+        ->condition($definition->getKey('label'), $match, 'CONTAINS')
+        ->condition($definition->getKey('id'), $match, 'CONTAINS')
+      );
+      $query->addTag($source_entity_type . '_access');
+      $entity_ids = $query->execute();
+
+      $entities = $storage->loadMultiple($entity_ids);
+      foreach ($entities as $source_entity_id => $source_entity) {
+        $label = Html::escape($this->entityManager()->getTranslationFromContext($source_entity)->label());
+        $value = "$label ($source_entity_type:$source_entity_id)";
+        $matches[] = [
+          'value' => $value,
+          'label' => $label,
+        ];
+
+        if (count($matches) === 10) {
+          new JsonResponse($matches);
+        }
+      }
+    }
+
+    return new JsonResponse($matches);
+  }
+
+}
diff --git a/web/modules/webform/src/Controller/WebformTestController.php b/web/modules/webform/src/Controller/WebformTestController.php
index f89450898b0354cae27579b3b9b8921c55661379..8e08f31681eeb9f132d10f059caeac6dc027c788 100644
--- a/web/modules/webform/src/Controller/WebformTestController.php
+++ b/web/modules/webform/src/Controller/WebformTestController.php
@@ -4,17 +4,27 @@
 
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\webform\Plugin\WebformHandler\EmailWebformHandler;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformRequestInterface;
 use Drupal\webform\WebformSubmissionGenerateInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 
 /**
  * Provides route responses for webform testing.
  */
 class WebformTestController extends ControllerBase implements ContainerInjectionInterface {
 
+  /**
+   * The messenger.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
   /**
    * Webform request handler.
    *
@@ -32,12 +42,15 @@ class WebformTestController extends ControllerBase implements ContainerInjection
   /**
    * Constructs a WebformTestController object.
    *
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger.
    * @param \Drupal\webform\WebformRequestInterface $request_handler
    *   The webform request handler.
    * @param \Drupal\webform\WebformSubmissionGenerateInterface $submission_generate
    *   The webform submission generation service.
    */
-  public function __construct(WebformRequestInterface $request_handler, WebformSubmissionGenerateInterface $submission_generate) {
+  public function __construct(MessengerInterface $messenger, WebformRequestInterface $request_handler, WebformSubmissionGenerateInterface $submission_generate) {
+    $this->messenger = $messenger;
     $this->requestHandler = $request_handler;
     $this->generate = $submission_generate;
   }
@@ -47,6 +60,7 @@ public function __construct(WebformRequestInterface $request_handler, WebformSub
    */
   public static function create(ContainerInterface $container) {
     return new static(
+      $container->get('messenger'),
       $container->get('webform.request'),
       $container->get('webform_submission.generate')
     );
@@ -65,6 +79,45 @@ public function testForm(Request $request) {
     /** @var \Drupal\webform\WebformInterface $webform */
     /** @var \Drupal\Core\Entity\EntityInterface $source_entity */
     list($webform, $source_entity) = $this->requestHandler->getWebformEntities();
+
+    // Test a single webform handler which is set via
+    // ?_webform_handler={handler_id}.
+    $test_webform_handler = $request->query->get('_webform_handler');
+    if ($test_webform_handler) {
+      // Make sure the handler exists.
+      if (!$webform->getHandlers()->has($test_webform_handler)) {
+        $t_args = [
+          '%webform' => $webform->label(),
+          '%handler' => $test_webform_handler,
+        ];
+        $this->messenger->addWarning($this->t('The %handler email/handler for the %webform webform does not exist.', $t_args));
+        throw new AccessDeniedHttpException();
+      }
+
+      // Enable only the selected handler for testing
+      // and disable all other handlers.
+      $handlers = $webform->getHandlers();
+      foreach ($handlers as $handler_id => $handler) {
+        if ($handler_id === $test_webform_handler) {
+          $handler->setStatus(TRUE);
+          $t_args = [
+            '%webform' => $webform->label(),
+            '%handler' => $handler->label(),
+            '@type' => ($handler instanceof EmailWebformHandler) ? $this->t('email') : $this->t('handler'),
+          ];
+          $this->messenger->addWarning($this->t('Testing the %webform webform %handler @type. <strong>All other emails/handlers are disabled.</strong>', $t_args));
+        }
+        else {
+          $handler->setStatus(FALSE);
+        }
+      }
+
+      // Set override to prevent the webform's altered handler statuses
+      // from being saved.
+      $webform->setOverride(TRUE);
+    }
+
+    // Set values.
     $values = [];
 
     // Set source entity type and id.
diff --git a/web/modules/webform/src/Element/Webform.php b/web/modules/webform/src/Element/Webform.php
index dbe34d9729f40cee4436e386ccb1d0aaeb85b549..6ec769fbb5e01f7b82f0f6fe8342444f937df97e 100644
--- a/web/modules/webform/src/Element/Webform.php
+++ b/web/modules/webform/src/Element/Webform.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\webform\Element;
 
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Render\Element\RenderElement;
 use Drupal\webform\Entity\Webform as WebformEntity;
 use Drupal\webform\WebformInterface;
@@ -24,7 +25,7 @@ public function getInfo() {
       ],
       '#webform' => NULL,
       '#default_data' => [],
-      '#cache' => ['max-age' => 0],
+      '#action' => NULL,
     ];
   }
 
@@ -33,13 +34,84 @@ public function getInfo() {
    */
   public static function preRenderWebformElement($element) {
     $webform = ($element['#webform'] instanceof WebformInterface) ? $element['#webform'] : WebformEntity::load($element['#webform']);
-    if (!$webform || !$webform->access('submission_create')) {
+    if (!$webform) {
       return $element;
     }
 
-    $values = ['data' => $element['#default_data']];
-    $element['webform_build'] = $webform->getSubmissionForm($values);
+    if ($webform->access('submission_create')) {
+      $values = [];
+
+      // Set data.
+      $values['data'] = $element['#default_data'];
+
+      // Set source entity type and id.
+      if (!empty($element['#entity']) && $element['#entity'] instanceof EntityInterface) {
+        $values['entity_type'] = $element['#entity']->getEntityTypeId();
+        $values['entity_id'] = $element['#entity']->id();
+      }
+      elseif (!empty($element['#entity_type']) && !empty($element['#entity_id'])) {
+        $values['entity_type'] = $element['#entity_type'];
+        $values['entity_id'] = $element['#entity_id'];
+      }
+
+      // Build the webform.
+      $element['webform_build'] = $webform->getSubmissionForm($values);
+
+      // Set custom form action.
+      if (!empty($element['#action'])) {
+        $element['webform_build']['#action'] = $element['#action'];
+      }
+    }
+    elseif ($webform->getSetting('form_access_denied') !== WebformInterface::ACCESS_DENIED_DEFAULT) {
+      // Set access denied message.
+      $element['webform_access_denied'] = static::buildAccessDenied($webform);
+    }
+    else {
+      // Add config and webform to cache contexts.
+      $config = \Drupal::configFactory()->get('webform.settings');
+      $renderer = \Drupal::service('renderer');
+      $renderer->addCacheableDependency($element, $config);
+      $renderer->addCacheableDependency($element, $webform);
+    }
+
     return $element;
   }
 
+  /**
+   * Build access denied message for a webform.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   *
+   * @return array
+   *   A renderable array containing thea ccess denied message for a webform.
+   */
+  public static function buildAccessDenied(WebformInterface $webform) {
+    /** @var \Drupal\webform\WebformTokenManagerInterface $webform_token_manager */
+    $webform_token_manager = \Drupal::service('webform.token_manager');
+
+    // Message.
+    $config = \Drupal::configFactory()->get('webform.settings');
+    $message = $webform->getSetting('form_access_denied_message')
+      ?: $config->get('settings.default_form_access_denied_message');
+    $message = $webform_token_manager->replace($message, $webform);
+
+    // Attributes.
+    $attributes = $webform->getSetting('form_access_denied_attributes');
+    $attributes['class'][] = 'webform-access-denied';
+
+    $build = [
+      '#type' => 'container',
+      '#attributes' => $attributes,
+      'message' => WebformHtmlEditor::checkMarkup($message),
+    ];
+
+    // Add config and webform to cache contexts.
+    $renderer = \Drupal::service('renderer');
+    $renderer->addCacheableDependency($build, $config);
+    $renderer->addCacheableDependency($build, $webform);
+
+    return $build;
+  }
+
 }
diff --git a/web/modules/webform/src/Element/WebformActions.php b/web/modules/webform/src/Element/WebformActions.php
index 53d63f3c9a9ceb699b1745cea121e0c868234229..a6d31a471dff136cc8a8827e61fb3c215c165817 100644
--- a/web/modules/webform/src/Element/WebformActions.php
+++ b/web/modules/webform/src/Element/WebformActions.php
@@ -5,6 +5,7 @@
 use Drupal\Component\Utility\Html;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element\Container;
+use Drupal\webform\Utility\WebformElementHelper;
 
 /**
  * Provides a wrapper element to group one or more Webform buttons in a form.
@@ -62,7 +63,7 @@ public static function processWebformActions(&$element, FormStateInterface $form
     $prefix = ($element['#webform_key']) ? 'edit-' . $element['#webform_key'] . '-' : '';
 
     // Add class names only if form['actions']['#type'] is set to 'actions'.
-    if (isset($complete_form['actions']['#type']) && $complete_form['actions']['#type'] == 'actions') {
+    if (WebformElementHelper::isType($complete_form['actions'], 'actions')) {
       $element['#attributes']['class'][] = 'form-actions';
       $element['#attributes']['class'][] = 'webform-actions';
     }
diff --git a/web/modules/webform/src/Element/WebformAddress.php b/web/modules/webform/src/Element/WebformAddress.php
index 8710c8af9e83f16839cfd196e17fe94e14c6c322..30d4a2f43f69e5118272e098af4206fbbf4a69d3 100644
--- a/web/modules/webform/src/Element/WebformAddress.php
+++ b/web/modules/webform/src/Element/WebformAddress.php
@@ -43,7 +43,7 @@ public static function getCompositeElements(array $element) {
     ];
     $elements['postal_code'] = [
       '#type' => 'textfield',
-      '#title' => t('Zip/Postal Code'),
+      '#title' => t('ZIP/Postal Code'),
     ];
     // Any webform options prefixed with 'country' will automatically
     // be included within the Composite Element UI.
diff --git a/web/modules/webform/src/Element/WebformButtons.php b/web/modules/webform/src/Element/WebformButtons.php
index 2ea08207cb0e01c60d06e4e118e96cfea3c2d2a8..cf3a5398e1bd96c3ff551bc4303f05dbe70175ab 100644
--- a/web/modules/webform/src/Element/WebformButtons.php
+++ b/web/modules/webform/src/Element/WebformButtons.php
@@ -22,14 +22,7 @@ public static function processRadios(&$element, FormStateInterface $form_state,
     $element['#attributes']['class'][] = 'webform-buttons';
     $element['#options_display'] = 'side_by_side';
 
-    if (floatval(\Drupal::VERSION) < 8.4) {
-      // Buttonset is deprecated jQueryUI 1.12
-      // https://api.jqueryui.com/buttonset/
-      $element['#attached']['library'][] = 'webform/webform.element.buttons.buttonset';
-    }
-    else {
-      $element['#attached']['library'][] = 'webform/webform.element.buttons.checkboxradio';
-    }
+    $element['#attached']['library'][] = 'webform/webform.element.buttons';
 
     return $element;
   }
diff --git a/web/modules/webform/src/Element/WebformButtonsOther.php b/web/modules/webform/src/Element/WebformButtonsOther.php
index ca2af45d8552802cdb07981999ffe3980ff9690a..086beaba1c7bb06a65085c0db5e8f0ecdd175398 100644
--- a/web/modules/webform/src/Element/WebformButtonsOther.php
+++ b/web/modules/webform/src/Element/WebformButtonsOther.php
@@ -25,13 +25,8 @@ class WebformButtonsOther extends WebformOtherBase {
    */
   public static function processWebformOther(&$element, FormStateInterface $form_state, &$complete_form) {
     // Attach element buttons before other handler.
-    // @see \Drupal\webform\Element\WebformButtons::processRadios
-    if (floatval(\Drupal::VERSION) < 8.4) {
-      $element['#attached']['library'][] = 'webform/webform.element.buttons.buttonset';
-    }
-    else {
-      $element['#attached']['library'][] = 'webform/webform.element.buttons.checkboxradio';
-    }
+    $element['#attached']['library'][] = 'webform/webform.element.buttons';
+
     $element['#wrapper_attributes']['class'][] = "js-webform-buttons";
     $element['#wrapper_attributes']['class'][] = "webform-buttons";
 
diff --git a/web/modules/webform/src/Element/WebformCodeMirror.php b/web/modules/webform/src/Element/WebformCodeMirror.php
index ed4030d9f1cee1d4fee630f45f5fd32b24c7d149..466456d8cbd627b62564ad3580cc5f1c1685635f 100644
--- a/web/modules/webform/src/Element/WebformCodeMirror.php
+++ b/web/modules/webform/src/Element/WebformCodeMirror.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Serialization\Yaml;
 use Drupal\Core\Render\Element\Textarea;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Entity\WebformSubmission;
 use Drupal\webform\Twig\TwigExtension;
 use Drupal\webform\Utility\WebformYaml;
 
@@ -47,6 +48,7 @@ public function getInfo() {
       '#skip_validation' => FALSE,
       '#cols' => 60,
       '#rows' => 5,
+      '#wrap' => TRUE,
       '#resizable' => 'vertical',
       '#process' => [
         [$class, 'processWebformCodeMirror'],
@@ -69,7 +71,7 @@ public static function valueCallback(&$element, $input, FormStateInterface $form
     if ($input === FALSE && $element['#mode'] == 'yaml' && isset($element['#default_value'])) {
       // Convert associative array in default value to YAML.
       if (is_array($element['#default_value'])) {
-        $element['#default_value'] = WebformYaml::tidy(Yaml::encode($element['#default_value']));
+        $element['#default_value'] = WebformYaml::encode($element['#default_value']);
       }
       // Convert empty YAML into an empty string.
       if ($element['#default_value'] == '{  }') {
@@ -102,6 +104,11 @@ public static function processWebformCodeMirror(&$element, FormStateInterface $f
       }
     }
 
+    // Set wrap off.
+    if (empty($element['#wrap'])) {
+      $element['#attributes']['wrap'] = 'off';
+    }
+
     // Add validate callback.
     $element += ['#element_validate' => []];
     array_unshift($element['#element_validate'], [get_called_class(), 'validateWebformCodeMirror']);
@@ -175,51 +182,13 @@ protected static function getErrors(&$element, FormStateInterface $form_state, &
 
     switch ($element['#mode']) {
       case 'html':
-        // @see: http://stackoverflow.com/questions/3167074/which-function-in-php-validate-if-the-string-is-valid-html
-        // @see: http://stackoverflow.com/questions/5030392/x-html-validator-in-php
-        libxml_use_internal_errors(TRUE);
-        if (simplexml_load_string('<fragment>' . $element['#value'] . '</fragment>')) {
-          return NULL;
-        }
-
-        $errors = libxml_get_errors();
-        libxml_clear_errors();
-        if (!$errors) {
-          return NULL;
-        }
-
-        $messages = [];
-        foreach ($errors as $error) {
-          $messages[] = $error->message;
-        }
-        return $messages;
+        return static::validateHtml($element, $form_state, $complete_form);
 
       case 'yaml':
-        try {
-          $value = $element['#value'];
-          $data = Yaml::decode($value);
-          if (!is_array($data) && $value) {
-            throw new \Exception(t('YAML must contain an associative array of elements.'));
-          }
-          return NULL;
-        }
-        catch (\Exception $exception) {
-          return [$exception->getMessage()];
-        }
+        return static::validateYaml($element, $form_state, $complete_form);
 
       case 'twig':
-        try {
-          $build = [
-            '#type' => 'inline_template',
-            '#template' => $element['#value'],
-            '#context' => [],
-          ];
-          \Drupal::service('renderer')->renderPlain($build);
-          return NULL;
-        }
-        catch (\Exception $exception) {
-          return [$exception->getMessage()];
-        }
+        return static::validateTwig($element, $form_state, $complete_form);
 
       default:
         return NULL;
@@ -239,4 +208,119 @@ public static function getMode($mode) {
     return (isset(static::$modes[$mode])) ? static::$modes[$mode] : static::$modes['text'];
   }
 
+  /****************************************************************************/
+  // Language/markup validation callback.
+  /****************************************************************************/
+
+  /**
+   * Validate HTML.
+   *
+   * @param array $element
+   *   The form element whose value is being validated.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   *
+   * @return array|null
+   *   An array of error messages.
+   */
+  protected static function validateHtml($element, FormStateInterface $form_state, $complete_form) {
+    // @see: http://stackoverflow.com/questions/3167074/which-function-in-php-validate-if-the-string-is-valid-html
+    // @see: http://stackoverflow.com/questions/5030392/x-html-validator-in-php
+    libxml_use_internal_errors(TRUE);
+    if (simplexml_load_string('<fragment>' . $element['#value'] . '</fragment>')) {
+      return NULL;
+    }
+
+    $errors = libxml_get_errors();
+    libxml_clear_errors();
+    if (!$errors) {
+      return NULL;
+    }
+
+    $messages = [];
+    foreach ($errors as $error) {
+      $messages[] = $error->message;
+    }
+    return $messages;
+  }
+
+  /**
+   * Validate Twig.
+   *
+   * @param array $element
+   *   The form element whose value is being validated.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   *
+   * @return array|null
+   *   An array of error messages.
+   */
+  protected static function validateTwig($element, FormStateInterface $form_state, $complete_form) {
+    $template = $element['#value'];
+    $form_object = $form_state->getFormObject();
+    try {
+      // If form object has ::getWebform method validate Twig template
+      // using a temporary webform submission context.
+      if (method_exists($form_object, 'getWebform')) {
+        /** @var \Drupal\webform\WebformInterface $webform */
+        $webform = $form_object->getWebform();
+
+        // Get a temporary webform submission.
+        /** @var \Drupal\webform\WebformSubmissionGenerateInterface $webform_submission_generate */
+        $webform_submission_generate = \Drupal::service('webform_submission.generate');
+        $values = [
+          // Set sid to 0 to prevent validation errors.
+          'sid' => 0,
+          'webform_id' => $webform->id(),
+          'data' => $webform_submission_generate->getData($webform),
+        ];
+        $webform_submission = WebformSubmission::create($values);
+        $build = TwigExtension::buildTwigTemplate($webform_submission, $template, []);
+      }
+      else {
+        $build = [
+          '#type' => 'inline_template',
+          '#template' => $element['#value'],
+          '#context' => [],
+        ];
+      }
+      \Drupal::service('renderer')->renderPlain($build);
+      return NULL;
+    }
+    catch (\Exception $exception) {
+      return [$exception->getMessage()];
+    }
+  }
+
+  /**
+   * Validate YAML.
+   *
+   * @param array $element
+   *   The form element whose value is being validated.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   *
+   * @return array|null
+   *   An array of error messages.
+   */
+  protected static function validateYaml($element, FormStateInterface $form_state, $complete_form) {
+    try {
+      $value = $element['#value'];
+      $data = Yaml::decode($value);
+      if (!is_array($data) && $value) {
+        throw new \Exception(t('YAML must contain an associative array of elements.'));
+      }
+      return NULL;
+    }
+    catch (\Exception $exception) {
+      return [$exception->getMessage()];
+    }
+  }
+
 }
diff --git a/web/modules/webform/src/Element/WebformCompositeBase.php b/web/modules/webform/src/Element/WebformCompositeBase.php
index 1207c6541af221d3009a72de17fbd0d0fc645a19..9465ec443ccf6a3bff2e6eca586cb2cbc59922d6 100644
--- a/web/modules/webform/src/Element/WebformCompositeBase.php
+++ b/web/modules/webform/src/Element/WebformCompositeBase.php
@@ -6,7 +6,6 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\Element\FormElement;
-use Drupal\webform\Plugin\WebformElement\WebformManagedFileBase as WebformManagedFileBasePlugin;
 use Drupal\webform\Utility\WebformElementHelper;
 
 /**
@@ -14,6 +13,8 @@
  */
 abstract class WebformCompositeBase extends FormElement implements WebformCompositeInterface {
 
+  use WebformCompositeFormElementTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -27,7 +28,7 @@ public function getInfo() {
         [$class, 'processAjaxForm'],
       ],
       '#pre_render' => [
-        [$class, 'preRenderCompositeFormElement'],
+        [$class, 'preRenderWebformCompositeFormElement'],
       ],
       '#title_display' => 'invisible',
       '#required' => FALSE,
@@ -213,13 +214,9 @@ public static function initializeCompositeElements(array &$element) {
    *
    * @param array $element
    *   A render array for the current element.
-   * @param array $element
+   * @param array $composite_elements
    *   A render array containing a composite's elements.
    *
-   * @return array
-   *   A renderable array of webform elements, containing the base properties
-   *   for the composite's webform elements.
-   *
    * @throws \Exception
    *   Throws exception when unsupported element type is used with a composite
    *   element.
@@ -257,13 +254,12 @@ protected static function initializeCompositeElementsRecursive(array &$element,
         $composite_element['#empty_option'] = $composite_element['#placeholder'];
       }
 
-      // Note: File uploads are not supported because uploaded file
-      // destination save and delete callbacks are not setup.
-      // @see \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::postSave
-      // @see \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::postDelete
-      if ($element_plugin instanceof WebformManagedFileBasePlugin) {
-        throw new \Exception('File upload element is not supported within composite elements.');
+      // Apply #select2 and #chosen to select elements.
+      if (isset($composite_element['#type']) && strpos($composite_element['#type'], 'select') !== FALSE) {
+        $select_properties = ['#select2' => '#select2', '#chosen' => '#chosen'];
+        $composite_element += array_intersect_key($element, $select_properties);
       }
+
       if ($element_plugin->hasMultipleValues($composite_element)) {
         throw new \Exception('Multiple elements are not supported within composite elements.');
       }
diff --git a/web/modules/webform/src/Element/WebformCompositeFormElementTrait.php b/web/modules/webform/src/Element/WebformCompositeFormElementTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..9dcb459252d9359052e55658e76b6759d432ae81
--- /dev/null
+++ b/web/modules/webform/src/Element/WebformCompositeFormElementTrait.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Drupal\webform\Element;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Render\Element;
+
+/**
+ * Provides a trait for webform composite form elements.
+ *
+ * Any form element that is comprised of several distinct parts can use this
+ * trait to add support for a composite title or description.
+ *
+ * The Webform overrides any element that is using the CompositeFormElementTrait
+ * and applies the below pre renderer which adds support for
+ * #wrapper_attributes and additional some classes.
+ *
+ * @see \Drupal\Core\Render\Element\CompositeFormElementTrait
+ * @see \Drupal\webform\Plugin\WebformElementBase::prepareCompositeFormElement
+ */
+trait WebformCompositeFormElementTrait {
+
+  /**
+   * Adds form element theming to an element if its title or description is set.
+   *
+   * This is used as a pre render function for checkboxes and radios.
+   */
+  public static function preRenderWebformCompositeFormElement($element) {
+    $has_content = (isset($element['#title']) || isset($element['#description']));
+    if (!$has_content) {
+      return $element;
+    }
+
+    // Set attributes.
+    if (!isset($element['#attributes'])) {
+      $element['#attributes'] = [];
+    }
+
+    // Apply wrapper attributes to attributes.
+    if (isset($element['#wrapper_attributes'])) {
+      $element['#attributes'] = NestedArray::mergeDeep($element['#attributes'], $element['#wrapper_attributes']);
+    }
+
+    // Set id and classes.
+    if (!isset($element['#attributes']['id'])) {
+      $element['#attributes']['id'] = $element['#id'] . '--wrapper';
+    }
+    $element['#attributes']['class'][] = Html::getClass($element['#type']) . '--wrapper';
+    $element['#attributes']['class'][] = 'fieldgroup';
+    $element['#attributes']['class'][] = 'form-composite';
+
+    // Add composite library.
+    $element['#attached']['library'][] = 'webform/webform.composite';
+
+    // Set theme wrapper to wrapper type.
+    $wrapper_type = (isset($element['#wrapper_type'])) ? $element['#wrapper_type'] : 'fieldset';
+    $element['#theme_wrappers'][] = $wrapper_type;
+
+    // Apply wrapper specific enhancements.
+    switch ($wrapper_type) {
+      case 'fieldset':
+        // Set the element's title attribute to show #title as a tooltip, if needed.
+        if (isset($element['#title']) && $element['#title_display'] == 'attribute') {
+          $element['#attributes']['title'] = $element['#title'];
+          if (!empty($element['#required'])) {
+            // Append an indication that this fieldset is required.
+            $element['#attributes']['title'] .= ' (' . t('Required') . ')';
+          }
+        }
+
+        // Add hidden and visible title class to fix composite fieldset
+        // top/bottom margins.
+        if (isset($element['#title'])) {
+          if (!empty($element['#title_display']) && in_array($element['#title_display'], ['invisible', 'attribute'])) {
+            $element['#attributes']['class'][] = 'webform-composite-hidden-title';
+          }
+          else {
+            $element['#attributes']['class'][] = 'webform-composite-visible-title';
+          }
+        }
+        break;
+
+      case 'form_element':
+        // Process #states for #wrapper_attributes.
+        // @see template_preprocess_form_element().
+        webform_process_states($element, '#wrapper_attributes');
+        break;
+    }
+
+    // Issue #3007132: [accessibility] Radios and checkboxes the WAI-ARIA
+    // 'aria-describedby' attribute has a reference to an ID that does not
+    // exist or an ID that is not unique
+    // https://www.drupal.org/project/webform/issues/3007132
+    // @see \Drupal\Core\Form\FormBuilder::doBuildForm
+    if (!empty($element['#description'])) {
+      $fix_aria_describedby = (preg_match('/^(?:webform_)?(?:radios|checkboxes|buttons)(?:_other)?$/', $element['#type']));
+      foreach (Element::children($element) as $key) {
+        // Skip if child element has a dedicated description.
+        if (!empty($element[$key]['#description'])) {
+          continue;
+        }
+
+        // Skip if 'aria-describedby' is not set.
+        if (empty($element[$key]['#attributes']['aria-describedby'])) {
+          continue;
+        }
+
+        // Only fix 'aria-describedby' attribute if it pointing to a broken id.
+        if ($element[$key]['#attributes']['aria-describedby'] === $element['#id'] . '--description') {
+          if ($fix_aria_describedby) {
+            $element[$key]['#attributes']['aria-describedby'] = $element['#attributes']['id'] . '--description';
+          }
+          else {
+            unset($element[$key]['#attributes']['aria-describedby']);
+          }
+        }
+      }
+    }
+
+    return $element;
+  }
+
+}
diff --git a/web/modules/webform/src/Element/WebformComputedBase.php b/web/modules/webform/src/Element/WebformComputedBase.php
index c8fe2111cd1d8b5f55c9b828dc7dbd29632e9c2a..45ad89a5e2b158ad13cf51cbab00c9be10e4e8ad 100644
--- a/web/modules/webform/src/Element/WebformComputedBase.php
+++ b/web/modules/webform/src/Element/WebformComputedBase.php
@@ -2,10 +2,14 @@
 
 namespace Drupal\webform\Element;
 
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element\FormElement;
+use Drupal\Core\Template\Attribute;
 use Drupal\webform\Entity\WebformSubmission;
 use Drupal\webform\Utility\WebformHtmlHelper;
+use Drupal\webform\Utility\WebformXss;
 use Drupal\webform\WebformSubmissionForm;
 use Drupal\webform\WebformSubmissionInterface;
 
@@ -35,6 +39,13 @@ abstract class WebformComputedBase extends FormElement {
    */
   const MODE_AUTO = 'auto';
 
+  /**
+   * Cache of submissions being processed.
+   *
+   * @var array
+   */
+  protected static $submissions = [];
+
   /**
    * {@inheritdoc}
    */
@@ -45,8 +56,13 @@ public function getInfo() {
         [$class, 'processWebformComputed'],
       ],
       '#input' => TRUE,
-      '#value' => '',
+      '#template' => '',
       '#mode' => NULL,
+      '#hide_empty' => FALSE,
+      // Note: Computed elements do not use the default #ajax wrapper, which is
+      // why we can use #ajax as a boolean.
+      // @see \Drupal\Core\Render\Element\RenderElement::preRenderAjaxForm
+      '#ajax' => FALSE,
       '#webform_submission' => NULL,
       '#theme_wrappers' => ['form_element'],
     ];
@@ -66,31 +82,80 @@ public function getInfo() {
    *   The processed element.
    */
   public static function processWebformComputed(&$element, FormStateInterface $form_state, &$complete_form) {
-    $webform_submission = static::getWebformSubmission($element, $form_state);
+    $webform_submission = static::getWebformSubmission($element, $form_state, $complete_form);
     if ($webform_submission) {
-      $value = static::processValue($element, $webform_submission);;
-
-      // Display markup.
-      $element['value']['#markup'] = $value;
-
-      // Include hidden element so that computed value will be available to
-      // conditions (#states).
+      // Set tree.
       $element['#tree'] = TRUE;
-      $element['hidden'] = [
-        '#type' => 'hidden',
-        '#value' => ['#markup' => $value],
-        '#parents' => $element['#parents'],
-      ];
 
       // Set #type to item to trigger #states behavior.
       // @see drupal_process_states;
       $element['#type'] = 'item';
+
+      $value = static::processValue($element, $webform_submission);
+      static::setWebformComputedElementValue($element, $value);
     }
 
     if (!empty($element['#states'])) {
       webform_process_states($element, '#wrapper_attributes');
     }
 
+    // Add validate callback.
+    $element += ['#element_validate' => []];
+    array_unshift($element['#element_validate'], [get_called_class(), 'validateWebformComputed']);
+
+    /**************************************************************************/
+    // Ajax support
+    /**************************************************************************/
+
+    // Enabled Ajax support only for computed elements associated with a
+    // webform submission form.
+    if ($element['#ajax'] && $form_state->getFormObject() instanceof WebformSubmissionForm) {
+      // Get button name and wrapper id.
+      $button_name = 'webform-computed-' . implode('-', $element['#parents']) . '-button';
+      $wrapper_id = 'webform-computed-' . implode('-', $element['#parents']) . '-wrapper';
+
+      // Get computed value element keys which are used to trigger Ajax updates.
+      preg_match_all('/(?:\[webform_submission:values:|data\.)([_a-z]+)/', $element['#template'], $matches);
+      $element_keys = $matches[1] ?: [];
+      $element_keys = array_unique($element_keys);
+
+      // Wrapping the computed element is two div tags.
+      // div.js-webform-computed is used to initialize the Ajax updates.
+      // div#wrapper_id is used to display response from the Ajax updates.
+      $element['#wrapper_id'] = $wrapper_id;
+      $element['#prefix'] = '<div class="js-webform-computed" data-webform-element-keys="' . implode(',', $element_keys) . '">' .
+        '<div class="js-webform-computed-wrapper" id="' . $wrapper_id . '">';
+      $element['#suffix'] = '</div></div>';
+
+      // Add hidden update button.
+      $element['update'] = [
+        '#type' => 'submit',
+        '#value' => t('Update'),
+        '#validate' => [[get_called_class(), 'validateWebformComputedCallback']],
+        '#submit' => [[get_called_class(), 'submitWebformComputedCallback']],
+        '#ajax' => [
+          'callback' => [get_called_class(), 'ajaxWebformComputedCallback'],
+          'wrapper' => $wrapper_id,
+          'progress' => ['type' => 'none'],
+        ],
+        // Disable validation, hide button, add submit button trigger class.
+        '#attributes' => [
+          'formnovalidate' => 'formnovalidate',
+          'class' => [
+            'js-hide',
+            'js-webform-computed-submit',
+          ],
+        ],
+        // Issue #1342066 Document that buttons with the same #value need a unique
+        // #name for the Form API to distinguish them, or change the Form API to
+        // assign unique #names automatically.
+        '#name' => $button_name,
+      ];
+
+      // Attached computed element Ajax library.
+      $element['#attached']['library'][] = 'webform/webform.element.computed';
+    }
+
     return $element;
   }
 
@@ -106,9 +171,151 @@ public static function processWebformComputed(&$element, FormStateInterface $for
    *   The string with tokens replaced.
    */
   public static function processValue(array $element, WebformSubmissionInterface $webform_submission) {
-    return $element['#value'];
+    return $element['#template'];
   }
 
+  /**
+   * Validates an computed element.
+   */
+  public static function validateWebformComputed(&$element, FormStateInterface $form_state, &$complete_form) {
+    // Make sure the form's state value uses the computed value and not the
+    // raw #value. This ensures conditional handlers are triggered using
+    // the accurate computed value.
+    $webform_submission = static::getWebformSubmission($element, $form_state, $complete_form);
+    if ($webform_submission) {
+      $value = static::processValue($element, $webform_submission);
+      $form_state->setValueForElement($element['value'], NULL);
+      $form_state->setValueForElement($element['hidden'], NULL);
+      $form_state->setValueForElement($element, $value);
+    }
+  }
+
+  /****************************************************************************/
+  // Form/Ajax callbacks.
+  /****************************************************************************/
+
+  /**
+   * Set computed element's value.
+   *
+   * @param array $element
+   *   A computed element.
+   * @param string $value
+   *   A computer value.
+   */
+  protected static function setWebformComputedElementValue(array &$element, $value) {
+    // Hide empty computed element using display:none so that #states API
+    // can still use the empty computed value.
+    if ($element['#hide_empty']) {
+      $element += ['#wrapper_attributes' => []];
+      $element['#wrapper_attributes'] += ['style' => ''];
+      if ($value === '') {
+        $element['#wrapper_attributes']['style'] .= ($element['#wrapper_attributes']['style'] ? ';' : '') . 'display:none';
+      }
+      else {
+        $element['#wrapper_attributes']['style'] = preg_replace('/;?display:none/', '', $element['#wrapper_attributes']['style']);
+      }
+    }
+
+    // Display markup.
+    $element['value']['#markup'] = $value;
+    $element['value']['#allowed_tags'] = WebformXss::getAdminTagList();
+
+    // Include hidden element so that computed value will be available to
+    // conditions (#states).
+    $element['hidden']['#type'] = 'hidden';
+    $element['hidden']['#value'] = ['#markup' => $value];
+    $element['hidden']['#parents'] = $element['#parents'];
+  }
+
+  /**
+   * Determine if the current request is using Ajax.
+   */
+  protected static function isAjax() {
+    return (\Drupal::request()->get(MainContentViewSubscriber::WRAPPER_FORMAT) === 'drupal_ajax');
+  }
+
+  /**
+   * Webform computed element validate callback.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public static function validateWebformComputedCallback(array $form, FormStateInterface $form_state) {
+    $form_state->clearErrors();
+  }
+
+  /**
+   * Webform computed element submit callback.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public static function submitWebformComputedCallback(array $form, FormStateInterface $form_state) {
+    // Only rebuild if the request is not using Ajax.
+    if (!static::isAjax()) {
+      $form_state->setRebuild();
+    }
+  }
+
+  /**
+   * Webform computed element Ajax callback.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   The computed element element.
+   */
+  public static function ajaxWebformComputedCallback(array $form, FormStateInterface $form_state) {
+    $button = $form_state->getTriggeringElement();
+    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
+
+    // Set element value and #markup  after the form has been validated.
+    $webform_submission = static::getWebformSubmission($element, $form_state, $form);
+    $value = static::processValue($element, $webform_submission);
+    static::setWebformComputedElementValue($element, $value);
+
+    // Only return the wrapper id, this prevents the computed element from
+    // being reinitialized via JS after each update.
+    // @see js/webform.element.computed.js
+    //
+    // The announce attribute allows FAPI Ajax callbacks to easily
+    // trigger announcements.
+    // @see js/webform.announce.js
+    $t_args = ['@title' => $element['#title'], '@value' => strip_tags($value)];
+    $attributes = [
+      'class' => ['js-webform-computed-wrapper'],
+      'id' => $element['#wrapper_id'],
+      'data-webform-announce' => t('@title is @value', $t_args),
+    ];
+    $element['#prefix'] = '<div' . new Attribute($attributes) . '>';
+
+    $element['#suffix'] = '</div>';
+
+    // Remove flexbox wrapper because it already been render outside this
+    // computed element's ajax wrapper.
+    // @see \Drupal\webform\Plugin\WebformElementBase::prepareWrapper
+    // @see \Drupal\webform\Plugin\WebformElementBase::preRenderFixFlexboxWrapper
+    $preRenderFixFlexWrapper = ['Drupal\webform\Plugin\WebformElement\WebformComputedTwig', 'preRenderFixFlexboxWrapper'];
+    foreach ($element['#pre_render'] as $index => $pre_render) {
+      if (is_array($pre_render) && $pre_render === $preRenderFixFlexWrapper) {
+        unset($element['#pre_render'][$index]);
+      }
+    }
+
+    return $element;
+  }
+
+  /****************************************************************************/
+  // Form/Ajax helpers and callbacks.
+  /****************************************************************************/
+
   /**
    * Get an element's value mode/type.
    *
@@ -120,7 +327,7 @@ public static function processValue(array $element, WebformSubmissionInterface $
    */
   public static function getMode(array $element) {
     if (empty($element['#mode']) || $element['#mode'] === static::MODE_AUTO) {
-      return (WebformHtmlHelper::containsHtml($element['#value'])) ? static::MODE_HTML : static::MODE_TEXT;
+      return (WebformHtmlHelper::containsHtml($element['#template'])) ? static::MODE_HTML : static::MODE_TEXT;
     }
     else {
       return $element['#mode'];
@@ -134,13 +341,35 @@ public static function getMode(array $element) {
    *   The element.
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
    *
    * @return \Drupal\webform\WebformSubmissionInterface|null
    *   A webform submission.
    */
-  protected static function getWebformSubmission(array $element, FormStateInterface $form_state) {
+  protected static function getWebformSubmission(array $element, FormStateInterface $form_state, array &$complete_form) {
     $form_object = $form_state->getFormObject();
-    if (isset($element['#webform_submission'])) {
+    if ($form_object instanceof WebformSubmissionForm) {
+      /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
+      $webform_submission = $form_object->getEntity();
+
+      // We must continually copy validated form values to the
+      // webform submission since a computed element's value can be based on
+      // another computed element's value.
+      //
+      // Therefore, we are creating a single clone of the webform submission
+      // and only copying the submitted form values to the cached submission.
+      if ($form_state->isValidationComplete()) {
+        if (!isset(static::$submissions[$webform_submission->uuid()])) {
+          static::$submissions[$webform_submission->uuid()] = clone $form_object->getEntity();
+        }
+        $webform_submission = static::$submissions[$webform_submission->uuid()];
+        $form_object->copyFormValuesToEntity($webform_submission, $complete_form, $form_state);
+      }
+
+      return $webform_submission;
+    }
+    elseif (isset($element['#webform_submission'])) {
       if (is_string($element['#webform_submission'])) {
         return WebformSubmission::load($element['#webform_submission']);
       }
@@ -148,9 +377,6 @@ protected static function getWebformSubmission(array $element, FormStateInterfac
         return $element['#webform_submission'];
       }
     }
-    elseif ($form_object instanceof WebformSubmissionForm) {
-      return $form_object->getEntity();
-    }
     else {
       return NULL;
     }
diff --git a/web/modules/webform/src/Element/WebformComputedToken.php b/web/modules/webform/src/Element/WebformComputedToken.php
index 0195047cdc78737ac8986870a410ffa6975a03cb..782242d0ad3f455f86fabcd2e347c3559e7f14cc 100644
--- a/web/modules/webform/src/Element/WebformComputedToken.php
+++ b/web/modules/webform/src/Element/WebformComputedToken.php
@@ -21,7 +21,7 @@ public static function processValue(array $element, WebformSubmissionInterface $
     $token_manager = \Drupal::service('webform.token_manager');
 
     // Replace tokens in value.
-    return $token_manager->replace($element['#value'], $webform_submission, [], ['html' => ($mode == static::MODE_HTML)]);
+    return $token_manager->replace($element['#template'], $webform_submission, [], ['html' => ($mode == static::MODE_HTML)]);
   }
 
 }
diff --git a/web/modules/webform/src/Element/WebformComputedTwig.php b/web/modules/webform/src/Element/WebformComputedTwig.php
index 70bc1acf354ccc97bad6519a0a4305ce9b45428f..69bd1ac483e5ee19f9657500857f82080f4d6300 100644
--- a/web/modules/webform/src/Element/WebformComputedTwig.php
+++ b/web/modules/webform/src/Element/WebformComputedTwig.php
@@ -12,14 +12,42 @@
  */
 class WebformComputedTwig extends WebformComputedBase {
 
+  /**
+   * Whitespace spaceless.
+   *
+   * Remove whitespace around the computed value and between HTML tags.
+   */
+  const WHITESPACE_SPACELESS = 'spaceless';
+
+  /**
+   * Whitespace trim.
+   *
+   * Remove whitespace around the computed value.
+   */
+  const WHITESPACE_TRIM = 'trim';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    return parent::getInfo() + [
+      '#whitespace' => '',
+    ];
+  }
+
   /**
    * {@inheritdoc}
    */
   public static function processValue(array $element, WebformSubmissionInterface $webform_submission) {
-    $template = $element['#value'];
+    $whitespace = (!empty($element['#whitespace'])) ? $element['#whitespace'] : '';
+
+    $template = ($whitespace === static::WHITESPACE_SPACELESS) ? '{% spaceless %}' . $element['#template'] . '{% endspaceless %}' : $element['#template'];
+
     $options = ['html' => (static::getMode($element) === static::MODE_HTML)];
 
-    return TwigExtension::renderTwigTemplate($webform_submission, $template, $options);
+    $value = TwigExtension::renderTwigTemplate($webform_submission, $template, $options);
+
+    return ($whitespace === static::WHITESPACE_TRIM) ? trim($value) : $value;
   }
 
 }
diff --git a/web/modules/webform/src/Element/WebformContact.php b/web/modules/webform/src/Element/WebformContact.php
index 02b083c3eff6fd70fabe6dc8297714f684d67670..436a7c3e009744923d0def43fa781d3b30986744 100644
--- a/web/modules/webform/src/Element/WebformContact.php
+++ b/web/modules/webform/src/Element/WebformContact.php
@@ -56,7 +56,7 @@ public static function getCompositeElements(array $element) {
     ];
     $elements['postal_code'] = [
       '#type' => 'textfield',
-      '#title' => t('Zip/Postal Code'),
+      '#title' => t('ZIP/Postal Code'),
     ];
     $elements['country'] = [
       '#type' => 'select',
diff --git a/web/modules/webform/src/Element/WebformElementAttributes.php b/web/modules/webform/src/Element/WebformElementAttributes.php
index 9903c1583e3d42d98b0ea73181056a8b90af1f22..7e982316426129fd347c1b099afc639352cf0012 100644
--- a/web/modules/webform/src/Element/WebformElementAttributes.php
+++ b/web/modules/webform/src/Element/WebformElementAttributes.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\webform\Element;
 
-use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element\FormElement;
 use Drupal\Core\Serialization\Yaml;
@@ -61,7 +60,7 @@ public static function processWebformElementAttributes(&$element, FormStateInter
 
     $t_args = [
       '@title' => $element['#title'],
-      '@type' => Unicode::strtolower($type),
+      '@type' => mb_strtolower($type),
     ];
 
     // Class.
@@ -71,18 +70,20 @@ public static function processWebformElementAttributes(&$element, FormStateInter
       $element['class'] = [
         '#type' => 'webform_select_other',
         '#title' => t('@title CSS classes', $t_args),
-        '#description' => t("Apply classes to the @type. Select 'custom...' to enter custom classes.", $t_args),
+        '#description' => t("Apply classes to the @type. Select 'custom…' to enter custom classes.", $t_args),
         '#multiple' => TRUE,
-        '#options' => [WebformSelectOther::OTHER_OPTION => t('custom...')] + array_combine($classes, $classes),
+        '#options' => [WebformSelectOther::OTHER_OPTION => t('custom…')] + array_combine($classes, $classes),
+        '#other__placeholder' => t('Enter custom classes…'),
         '#other__option_delimiter' => ' ',
         '#attributes' => [
           'class' => [
             'js-' . $element['#id'] . '-attributes-style',
           ],
         ],
+        '#select2' => TRUE,
         '#default_value' => $element['#default_value']['class'],
       ];
-      WebformElementHelper::enhanceSelect($element['class'], TRUE);
+      WebformElementHelper::process($element['class']);
     }
     else {
       $element['class'] = [
@@ -96,7 +97,7 @@ public static function processWebformElementAttributes(&$element, FormStateInter
     // Custom options.
     $element['custom'] = [
       '#type' => 'texfield',
-      '#placeholder' => t('Enter custom classes...'),
+      '#placeholder' => t('Enter custom classes…'),
       '#states' => [
         'visible' => [
           'select.js-' . $element['#id'] . '-attributes-style' => ['value' => '_custom_'],
@@ -123,7 +124,7 @@ public static function processWebformElementAttributes(&$element, FormStateInter
       '#title' => t('@title custom attributes (YAML)', $t_args),
       '#description' => t('Enter additional attributes to be added the @type.', $t_args),
       '#attributes__access' => (!\Drupal::moduleHandler()->moduleExists('webform_ui') || \Drupal::currentUser()->hasPermission('edit webform source')),
-      '#default_value' => WebformYaml::tidy(Yaml::encode($attributes)),
+      '#default_value' => WebformYaml::encode($attributes),
     ];
 
     // Apply custom properties. Typically used for descriptions.
diff --git a/web/modules/webform/src/Element/WebformElementComposite.php b/web/modules/webform/src/Element/WebformElementComposite.php
index bfeb6c9b2450bebfd79830cd2ed0e96ee7089ab5..c21bfea633814a0d7b9497f05e9efcf0b0d78853 100644
--- a/web/modules/webform/src/Element/WebformElementComposite.php
+++ b/web/modules/webform/src/Element/WebformElementComposite.php
@@ -2,12 +2,13 @@
 
 namespace Drupal\webform\Element;
 
-use Drupal\Component\Serialization\Yaml;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element\FormElement;
+use Drupal\Core\Serialization\Yaml;
 use Drupal\webform\Plugin\WebformElement\WebformCompositeBase as WebformCompositeBaseElement;
 use Drupal\webform\Utility\WebformArrayHelper;
+use Drupal\webform\Utility\WebformYaml;
 
 /**
  * Provides a element for the composite elements.
@@ -17,7 +18,7 @@
 class WebformElementComposite extends FormElement {
 
   /**
-   * List of supported  element properties.
+   * List of supported element properties.
    *
    * @var array
    */
@@ -66,7 +67,7 @@ public static function valueCallback(&$element, $input, FormStateInterface $form
           $composite_properties = array_intersect_key($composite_element, static::$supportedProperties);
           // Move 'unsupported' properties to 'custom'.
           $custom_properties = array_diff_key($composite_element, static::$supportedProperties);
-          $composite_properties['custom'] = $custom_properties ? trim(Yaml::encode($custom_properties)) : '';
+          $composite_properties['custom'] = $custom_properties ? WebformYaml::encode($custom_properties) : '';
           $default_value[] = $composite_properties;
         }
         $element['#default_value'] = $default_value;
@@ -122,26 +123,29 @@ public static function processWebformElementComposite(&$element, FormStateInterf
     $element['elements'] = [
       '#type' => 'webform_multiple',
       '#title' => t('Elements'),
-      '#title_display' => t('Invisible'),
+      '#title_display' => 'invisible',
       '#label' => t('element'),
       '#labels' => t('elements'),
       '#empty_items' => 0,
+      '#min_items' => 1,
       '#header' => TRUE,
+      '#add' => FALSE,
       '#default_value' => (isset($element['#default_value'])) ? $element['#default_value'] : NULL,
       '#error_no_message' => TRUE,
       '#element' => [
-        'key_type_options' => [
+        'settings' => [
           '#type' => 'container',
-          '#title' => ($edit_source) ? t('Key / Type / Options / Custom Properties') : t('Key / Type / Options '),
+          '#title' => t('Settings'),
           '#help' => '<b>' . t('Key') . ':</b> ' . t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.') .
-            '<br/><br/>' . '<b>' . t('Type') . ':</b> ' . t('The type of element to be displayed.') .
-            '<br/><br/>' . '<b>' . t('Options') . ':</b> ' . t('Please select predefined options or enter custom options.') . ' ' . t('Key-value pairs MUST be specified as "safe_key: \'Some readable options\'". Use of only alphanumeric characters and underscores is recommended in keys. One option per line.') .
-            ($edit_source ? '<br/><br/>' . '<b>' . t('Custom Properties') . ':</b> ' . t('Properties do not have to be prepended with a hash (#) character, the hash character will be automatically added to the custom properties.') : ''),
+            '<hr/>' . '<b>' . t('Type') . ':</b> ' . t('The type of element to be displayed.') .
+            '<hr/>' . '<b>' . t('Options') . ':</b> ' . t('Please select predefined options or enter custom options.') . ' ' . t('Key-value pairs MUST be specified as "safe_key: \'Some readable options\'". Use of only alphanumeric characters and underscores is recommended in keys. One option per line.') .
+            ($edit_source ? '<hr/>' . '<b>' . t('Custom Properties') . ':</b> ' . t('Properties do not have to be prepended with a hash (#) character, the hash character will be automatically added to the custom properties.') : '') .
+            '<hr/>' . '<b>' . t('Required') . ':</b> ' . t('Check this option if the user must enter a value.'),
           'key' => [
             '#type' => 'textfield',
             '#title' => t('Key'),
             '#title_display' => 'invisible',
-            '#placeholder' => t('Enter key'),
+            '#placeholder' => t('Enter key…'),
             '#pattern' => '^[a-z0-9_]+$',
             '#attributes' => [
               'title' => t('Enter a unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'),
@@ -153,6 +157,8 @@ public static function processWebformElementComposite(&$element, FormStateInterf
             '#type' => 'select',
             '#title' => t('Type'),
             '#title_display' => 'invisible',
+            '#description' => t('The type of element to be displayed.'),
+            '#description_display' => 'invisible',
             '#options' => $type_options,
             '#empty_option' => t('- Select type -'),
             '#required' => TRUE,
@@ -164,6 +170,8 @@ public static function processWebformElementComposite(&$element, FormStateInterf
             '#yaml' => TRUE,
             '#title' => t('Options'),
             '#title_display' => 'invisible',
+            '#description' => t('Please select predefined options or enter custom options.') . ' ' . t('Key-value pairs MUST be specified as "safe_key: \'Some readable options\'". Use of only alphanumeric characters and underscores is recommended in keys. One option per line.'),
+            '#description_display' => 'invisible',
             '#wrapper_attributes' => [
               'data-composite-types' => implode(',', $options_elements),
               'data-composite-required' => 'data-composite-required',
@@ -181,25 +189,36 @@ public static function processWebformElementComposite(&$element, FormStateInterf
             '#mode' => 'yaml',
             '#title' => t('Custom properties'),
             '#title_display' => 'invisible',
-            '#placeholder' => t('Enter custom properties'),
+            '#description' => t('Properties do not have to be prepended with a hash (#) character, the hash character will be automatically added to the custom properties.'),
+            '#description_display' => 'invisible',
+            '#placeholder' => t('Enter custom properties…'),
             '#error_no_message' => TRUE,
           ] : [
             '#type' => 'hidden',
           ],
+          // Note: Setting #return_value: TRUE is not returning any value.
+          'required' => [
+            '#type' => 'checkbox',
+            '#title' => t('Required'),
+            '#description' => t('Check this option if the user must enter a value.'),
+            '#description_display' => 'invisible',
+            '#error_no_message' => TRUE,
+          ],
         ],
-        'title_placeholder_description_help' => [
+        'labels' => [
           '#type' => 'container',
-          '#title' => t('Title / Placeholder / Description / Help'),
-          '#help' => '<b>' . t('Title') . ':</b> ' . t('This is used as a descriptive label when displaying this webform element.') . '<br/><br/>' .
-            '<b>' . t('Placeholder') . ':</b> ' . t('The placeholder will be shown in the element until the user starts entering a value.') . '<br/><br/>' .
-            '<b>' . t('Description') . ':</b> ' . t('A short description of the element used as help for the user when he/she uses the webform.') . '<br/><br/>' .
-            '<b>' . t('Help text') . ':</b> ' . t('A tooltip displayed after the title.'),
-
+          '#title' => t('Labels'),
+          '#help' => '<b>' . t('Title') . ':</b> ' . t('This is used as a descriptive label when displaying this webform element.') .
+            '<hr/><b>' . t('Placeholder') . ':</b> ' . t('The placeholder will be shown in the element until the user starts entering a value.') .
+            '<hr/><b>' . t('Description') . ':</b> ' . t('A short description of the element used as help for the user when he/she uses the webform.') .
+            '<hr/><b>' . t('Help text') . ':</b> ' . t('A tooltip displayed after the title.'),
           'title' => [
             '#type' => 'textfield',
             '#title' => t('Title'),
             '#title_display' => 'invisible',
-            '#placeholder' => t('Enter title'),
+            '#description' => t('This is used as a descriptive label when displaying this webform element.'),
+            '#description_display' => 'invisible',
+            '#placeholder' => t('Enter title…'),
             '#required' => TRUE,
             '#error_no_message' => TRUE,
           ],
@@ -207,15 +226,19 @@ public static function processWebformElementComposite(&$element, FormStateInterf
             '#type' => 'textfield',
             '#title' => t('Placeholder'),
             '#title_display' => 'invisible',
-            '#placeholder' => t('Enter placeholder'),
+            '#description' => t('The placeholder will be shown in the element until the user starts entering a value.'),
+            '#description_display' => 'invisible',
+            '#placeholder' => t('Enter placeholder…'),
             '#attributes' => ['data-composite-types' => implode(',', $placeholder_elements)],
             '#error_no_message' => TRUE,
           ],
           'description' => [
             '#type' => 'textarea',
             '#title' => t('Description'),
+            '#description' => t('A short description of the element used as help for the user when he/she uses the webform.'),
+            '#description_display' => 'invisible',
             '#title_display' => 'invisible',
-            '#placeholder' => t('Enter description'),
+            '#placeholder' => t('Enter description…'),
             '#rows' => 2,
             '#error_no_message' => TRUE,
           ],
@@ -223,19 +246,13 @@ public static function processWebformElementComposite(&$element, FormStateInterf
             '#type' => 'textarea',
             '#title' => t('Help text'),
             '#title_display' => 'invisible',
-            '#placeholder' => t('Enter help text'),
+            '#description' => t('A tooltip displayed after the title.'),
+            '#description_display' => 'invisible',
+            '#placeholder' => t('Enter help text…'),
             '#rows' => 2,
             '#error_no_message' => TRUE,
           ],
         ],
-        // Note: Setting #return_value: TRUE is not returning any value.
-        'required' => [
-          '#type' => 'checkbox',
-          '#title' => t('Req.'),
-          '#title_display' => 'invisible',
-          '#help' => '<b>' . t('Required') . ':</b> ' . t('Check this option if the user must enter a value.'),
-          '#error_no_message' => TRUE,
-        ],
       ],
     ];
 
diff --git a/web/modules/webform/src/Element/WebformElementOptions.php b/web/modules/webform/src/Element/WebformElementOptions.php
index 617c20a8f6aaaacfa1dbec42e53f49b664e98fbb..0726c91c9ba15616e2af71f197d828357c4be976 100644
--- a/web/modules/webform/src/Element/WebformElementOptions.php
+++ b/web/modules/webform/src/Element/WebformElementOptions.php
@@ -9,6 +9,7 @@
 use Drupal\Core\Url;
 use Drupal\webform\Entity\WebformOptions as WebformOptionsEntity;
 use Drupal\webform\Utility\WebformElementHelper;
+use Drupal\webform\Utility\WebformOptionsHelper;
 
 /**
  * Provides a form element for managing webform element options.
@@ -84,20 +85,22 @@ public static function processWebformElementOptions(&$element, FormStateInterfac
       ':href' => Url::fromRoute('entity.webform_options.collection')->toString(),
     ];
 
+    $has_options = (count($options)) ? TRUE : FALSE;
+
     // Select options.
     $element['options'] = [
       '#type' => 'select',
       '#description' => t('Please select <a href=":href">predefined @type</a> or enter custom @type.', $t_args),
       '#options' => [
-        self::CUSTOM_OPTION => t('Custom @type...', $t_args),
+        self::CUSTOM_OPTION => t('Custom @type…', $t_args),
       ] + $options,
 
       '#attributes' => [
         'class' => ['js-' . $element['#id'] . '-options'],
       ],
       '#error_no_message' => TRUE,
-      '#access' => count($options) ? TRUE : FALSE,
-      '#default_value' => (isset($element['#default_value']) && !is_array($element['#default_value'])) ? $element['#default_value'] : '',
+      '#access' => $has_options,
+      '#default_value' => (isset($element['#default_value']) && !is_array($element['#default_value']) && WebformOptionsHelper::hasOption($element['#default_value'], $options)) ? $element['#default_value'] : '',
     ];
 
     // Custom options.
@@ -106,11 +109,6 @@ public static function processWebformElementOptions(&$element, FormStateInterfac
         '#type' => 'webform_multiple',
         '#title' => $element['#title'],
         '#title_display' => 'invisible',
-        '#states' => [
-          'visible' => [
-            'select.js-' . $element['#id'] . '-options' => ['value' => ''],
-          ],
-        ],
         '#error_no_message' => TRUE,
         '#default_value' => (isset($element['#default_value']) && !is_string($element['#default_value'])) ? $element['#default_value'] : [],
       ];
@@ -123,16 +121,19 @@ public static function processWebformElementOptions(&$element, FormStateInterfac
         '#title_display' => 'invisible',
         '#label' => ($element['#likert']) ? t('answer') : t('option'),
         '#labels' => ($element['#likert']) ? t('answers') : t('options'),
-        '#states' => [
-          'visible' => [
-            'select.js-' . $element['#id'] . '-options' => ['value' => ''],
-          ],
-        ],
         '#error_no_message' => TRUE,
         '#options_description' => $element['#options_description'],
         '#default_value' => (isset($element['#default_value']) && !is_string($element['#default_value'])) ? $element['#default_value'] : [],
       ];
     }
+    // If there are options set #states.
+    if ($has_options) {
+      $element['custom']['#states'] = [
+        'visible' => [
+          'select.js-' . $element['#id'] . '-options' => ['value' => self::CUSTOM_OPTION],
+        ],
+      ];
+    }
 
     // Add validate callback.
     $element += ['#element_validate' => []];
diff --git a/web/modules/webform/src/Element/WebformElementStates.php b/web/modules/webform/src/Element/WebformElementStates.php
index 2515667f9ae67b6697fe3a20f47e366afec01183..8830f8ea349a4478415211059864036e1537b9df 100644
--- a/web/modules/webform/src/Element/WebformElementStates.php
+++ b/web/modules/webform/src/Element/WebformElementStates.php
@@ -2,12 +2,13 @@
 
 namespace Drupal\webform\Element;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Form\OptGroup;
 use Drupal\Core\Serialization\Yaml;
-use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Render\Element\FormElement;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Utility\WebformAccessibilityHelper;
 use Drupal\webform\Utility\WebformArrayHelper;
 use Drupal\webform\Utility\WebformYaml;
 
@@ -26,6 +27,7 @@ public function getInfo() {
     return [
       '#input' => TRUE,
       '#selector_options' => [],
+      '#selector_sources' => [],
       '#empty_states' => 3,
       '#process' => [
         [$class, 'processWebformStates'],
@@ -65,6 +67,7 @@ public static function valueCallback(&$element, $input, FormStateInterface $form
    * Expand an email confirm field into two HTML5 email elements.
    */
   public static function processWebformStates(&$element, FormStateInterface $form_state, &$complete_form) {
+
     // Define default #state_options and #trigger_options.
     // There are also defined by \Drupal\webform\Plugin\WebformElementBase::form.
     $element += [
@@ -74,23 +77,43 @@ public static function processWebformStates(&$element, FormStateInterface $form_
 
     $element['#tree'] = TRUE;
 
+    $edit_source = $form_state->get(static::getStorageKey($element, 'edit_source'));
+
     // Add validate callback that extracts the associative array of states.
     $element += ['#element_validate' => []];
     array_unshift($element['#element_validate'], [get_called_class(), 'validateWebformElementStates']);
 
     // For customized #states display a CodeMirror YAML editor.
-    if ($warning_message = static::isDefaultValueCustomizedFormApiStates($element)) {
-      $warning_message .= ' ' . t('Form API #states must be manually entered.');
-      $element['messages'] = [
-        '#type' => 'webform_message',
-        '#message_type' => 'warning',
-        '#message_message' => $warning_message,
-      ];
+    $warning_message = static::isDefaultValueCustomizedFormApiStates($element);
+    if ($warning_message || $edit_source) {
+      if ($warning_message) {
+        $warning_message .= ' ' . t('Form API #states must be manually entered.');
+        $element['warning_messages'] = [
+          '#type' => 'webform_message',
+          '#message_type' => 'warning',
+          '#message_message' => $warning_message,
+        ];
+      }
+
+      if ($edit_source) {
+        $element['edit_source_message'] = [
+          '#type' => 'webform_message',
+          '#message_message' => t('Creating custom conditional logic (Form API #states) with nested conditions or custom selectors will disable the conditional logic builder. This will require that Form API #states be manually entered.'),
+          '#message_type' => 'info',
+          '#message_close' => TRUE,
+          '#message_storage' => WebformMessage::STORAGE_SESSION,
+        ];
+      }
       $element['states'] = [
         '#type' => 'webform_codemirror',
+        '#title' => t('Conditional Logic (YAML)'),
+        '#title_display' => 'invisible',
         '#mode' => 'yaml',
-        '#default_value' => WebformYaml::tidy(Yaml::encode($element['#default_value'])),
+        '#default_value' => WebformYaml::encode($element['#default_value']),
         '#description' => t('Learn more about Drupal\'s <a href=":href">Form API #states</a>.', [':href' => 'https://www.lullabot.com/articles/form-api-states']),
+        '#webform_element' => TRUE,
+        '#more_title' => t('Help'),
+        '#more' => static::buildSourceHelp($element),
       ];
       return $element;
     }
@@ -121,9 +144,9 @@ public static function processWebformStates(&$element, FormStateInterface $form_
     // Build header.
     $header = [
       ['data' => t('State'), 'width' => '25%'],
-      ['data' => t('Element/Selector'), 'width' => '50%'],
+      ['data' => t('Element'), 'width' => '50%'],
       ['data' => t('Trigger/Value'), 'width' => '25%'],
-      ['data' => ''],
+      ['data' => WebformAccessibilityHelper::buildVisuallyHidden(t('Operations'))],
     ];
 
     // Get states and number of rows.
@@ -134,11 +157,15 @@ public static function processWebformStates(&$element, FormStateInterface $form_
       $states = (isset($element['#default_value'])) ? static::convertFormApiStatesToStatesArray($element['#default_value']) : [];
     }
 
+    // Track state row indexes for disable/enabled warning message.
+    $state_row_indexes = [];
+
     // Build state and conditions rows.
     $row_index = 0;
     $rows = [];
     foreach ($states as $state_settings) {
       $rows[$row_index] = static::buildStateRow($element, $state_settings, $table_id, $row_index, $ajax_settings);
+      $state_row_indexes[] = $row_index;
       $row_index++;
       foreach ($state_settings['conditions'] as $condition) {
         $rows[$row_index] = static::buildConditionRow($element, $condition, $table_id, $row_index, $ajax_settings);
@@ -148,7 +175,8 @@ public static function processWebformStates(&$element, FormStateInterface $form_
 
     // Generator empty state with conditions rows.
     if ($row_index < $number_of_rows) {
-      $rows[$row_index] = static::buildStateRow($element, [], $table_id, $row_index, $ajax_settings);;
+      $rows[$row_index] = static::buildStateRow($element, [], $table_id, $row_index, $ajax_settings);
+      $state_row_indexes[] = $row_index;
       $row_index++;
       while ($row_index < $number_of_rows) {
         $rows[$row_index] = static::buildConditionRow($element, [], $table_id, $row_index, $ajax_settings);
@@ -156,17 +184,22 @@ public static function processWebformStates(&$element, FormStateInterface $form_
       }
     }
 
+    // Add wrapper to the element.
+    $element += ['#prefix' => '', '#suffix' => ''];
+    $element['#prefix'] = '<div id="' . $table_id . '">' . $element['#prefix'];
+    $element['#suffix'] .= '</div>';
+
     // Build table.
     $element['states'] = [
-      '#prefix' => '<div id="' . $table_id . '" class="webform-states-table">',
-      '#suffix' => '</div>',
       '#type' => 'table',
       '#header' => $header,
+      '#attributes' => ['class' => ['webform-states-table']],
     ] + $rows;
 
+    $element['actions'] = ['#type' => 'container'];
     // Build add state action.
     if ($element['#multiple']) {
-      $element['add'] = [
+      $element['actions']['add'] = [
         '#type' => 'submit',
         '#value' => t('Add another state'),
         '#limit_validation_errors' => [],
@@ -176,11 +209,145 @@ public static function processWebformStates(&$element, FormStateInterface $form_
       ];
     }
 
+    // Edit source.
+    if (\Drupal::currentUser()->hasPermission('edit webform source')) {
+      $element['actions']['source'] = [
+        '#type' => 'submit',
+        '#value' => t('Edit source'),
+        '#limit_validation_errors' => [],
+        '#submit' => [[get_called_class(), 'editSourceSubmit']],
+        '#ajax' => $ajax_settings,
+        '#attributes' => ['class' => ['button', 'button--danger']],
+        '#name' => $table_id . '_source',
+      ];
+    }
+
+    // Display a warning message when any state is set to disabled or enabled.
+    if (!empty($element['#disabled_message'])) {
+      $total_state_row_indexes = count($state_row_indexes);
+      $triggers = [];
+      foreach ($state_row_indexes as $index => $row_index) {
+        $id = Html::getId('edit-' . implode('-', $element['#parents']) . '-states-' . $row_index . '-state');
+        $triggers[] = [':input[data-drupal-selector="' . $id . '"]' => ['value' => ['pattern' => '^(disabled|enabled)$']]];
+        if (($index + 1) < $total_state_row_indexes) {
+          $triggers[] = 'or';
+        }
+      }
+      if ($triggers) {
+        $element['disabled_message'] = [
+          '#type' => 'webform_message',
+          '#message_message' => t('<a href="https://www.w3schools.com/tags/att_input_disabled.asp">Disabled</a> elements do not submit data back to the server and the element\'s server-side default or current value will be preserved and saved to the database.'),
+          '#message_type' => 'warning',
+          '#states' => ['visible' => $triggers],
+        ];
+      }
+    }
+
     $element['#attached']['library'][] = 'webform/webform.element.states';
 
+    // Convert #options to jQuery autocomplete source format.
+    // @see http://api.jqueryui.com/autocomplete/#option-source
+    $selectors = [];
+    $sources = [];
+    if ($element['#selector_sources']) {
+      foreach ($element['#selector_sources'] as $selector => $values) {
+        $sources_key = md5(serialize($values));
+        $selectors[$selector] = $sources_key;
+        if (!isset($sources[$sources_key])) {
+          foreach ($values as $key => $value) {
+            $sources[$sources_key][] = [
+              'label' => (string) $value . ($value != $key ? ' (' . $key . ')' : ''),
+              'value' => (string) $key,
+            ];
+          }
+        }
+      }
+    }
+    $element['#attached']['drupalSettings']['webformElementStates'] = [
+      'selectors' => $selectors,
+      'sources' => $sources,
+    ];
+
     return $element;
   }
 
+  /**
+   * Build edit source help.
+   *
+   * @param array $element
+   *   An element.
+   *
+   * @return array
+   *   A renderable array.
+   */
+  protected static function buildSourceHelp(array $element) {
+    $build = [];
+    $build['states'] = [
+      'title' => [
+        '#markup' => t('Available states'),
+        '#prefix' => '<strong>',
+        '#suffix' => '</strong>',
+      ],
+      'items' => static::convertOptionToItemList($element['#state_options']),
+    ];
+    if ($element['#selector_options']) {
+      $build['selectors'] = [
+        'title' => [
+          '#markup' => t('Available selectors'),
+          '#prefix' => '<strong>',
+          '#suffix' => '</strong>',
+        ],
+        'items' => static::convertOptionToItemList($element['#selector_options']),
+      ];
+    }
+    $build['triggers'] = [
+      'title' => [
+        '#markup' => t('Available triggers'),
+        '#prefix' => '<strong>',
+        '#suffix' => '</strong>',
+      ],
+      'items' => static::convertOptionToItemList($element['#trigger_options']),
+    ];
+    return $build;
+  }
+
+  /**
+   * Convert options with optgroup to item list.
+   *
+   * @param array $options
+   *   An array of options.
+   *
+   * @return array
+   *   A renderable array.
+   */
+  protected static function convertOptionToItemList(array $options) {
+    $items = [];
+    foreach ($options as $option_name => $option_value) {
+      if (is_array($option_value)) {
+        $items[$option_name] = [
+          'title' => [
+            '#markup' => $option_name,
+          ],
+          'children' => [
+            '#theme' => 'item_list',
+            '#items' => array_keys($option_value),
+          ],
+        ];
+      }
+      else {
+        $items[$option_name] = [
+          '#markup' => $option_name,
+          '#prefix' => '<div>',
+          '#suffix' => '</div>',
+        ];
+      }
+    }
+    return [
+      '#theme' => 'item_list',
+      '#items' => $items,
+    ];
+  }
+
   /**
    * Build state row.
    *
@@ -207,13 +374,18 @@ protected static function buildStateRow(array $element, array $state, $table_id,
     ];
     $row['state'] = [
       '#type' => 'select',
+      '#title' => t('State'),
+      '#title_display' => 'invisible',
       '#options' => $element['#state_options'],
       '#default_value' => $state['state'],
       '#empty_option' => t('- Select -'),
       '#wrapper_attributes' => ['class' => ['webform-states-table--state']],
+      '#error_no_message' => TRUE,
     ];
     $row['operator'] = [
       '#type' => 'select',
+      '#title' => t('Operator'),
+      '#title_display' => 'invisible',
       '#options' => [
         'and' => t('All'),
         'or' => t('Any'),
@@ -223,6 +395,7 @@ protected static function buildStateRow(array $element, array $state, $table_id,
       '#field_prefix' => t('if'),
       '#field_suffix' => t('of the following is met:'),
       '#wrapper_attributes' => ['class' => ['webform-states-table--operator'], 'colspan' => 2, 'align' => 'left'],
+      '#error_no_message' => TRUE,
     ];
     $row['operations'] = static::buildOperations($table_id, $row_index, $ajax_settings);
     if (!$element['#multiple']) {
@@ -262,10 +435,13 @@ protected static function buildConditionRow(array $element, array $condition, $t
     $row['state'] = [];
     $row['selector'] = [
       '#type' => 'select',
+      '#title' => t('Selector'),
+      '#title_display' => 'invisible',
       '#options' => $element['#selector_options'],
       '#wrapper_attributes' => ['class' => ['webform-states-table--selector']],
       '#default_value' => $condition['selector'],
       '#empty_option' => t('- Select -'),
+      '#error_no_message' => TRUE,
     ];
     if (!isset($element['#selector_options'][$condition['selector']])) {
       $row['selector']['#options'][$condition['selector']] = $condition['selector'];
@@ -275,11 +451,14 @@ protected static function buildConditionRow(array $element, array $condition, $t
     ];
     $row['condition']['trigger'] = [
       '#type' => 'select',
+      '#title' => t('Trigger'),
+      '#title_display' => 'invisible',
       '#options' => $element['#trigger_options'],
       '#default_value' => $condition['trigger'],
       '#empty_option' => t('- Select -'),
       '#parents' => [$element_name, 'states', $row_index , 'trigger'],
       '#wrapper_attributes' => ['class' => ['webform-states-table--trigger']],
+      '#error_no_message' => TRUE,
     ];
     $row['condition']['value'] = [
       '#type' => 'textfield',
@@ -287,7 +466,7 @@ protected static function buildConditionRow(array $element, array $condition, $t
       '#title_display' => 'invisible',
       '#size' => 25,
       '#default_value' => $condition['value'],
-      '#placeholder' => t('Enter value...'),
+      '#placeholder' => t('Enter value…'),
       '#states' => [
         'visible' => [
           [$trigger_selector => ['value' => 'value']],
@@ -305,6 +484,7 @@ protected static function buildConditionRow(array $element, array $condition, $t
       ],
       '#wrapper_attributes' => ['class' => ['webform-states-table--value']],
       '#parents' => [$element_name, 'states', $row_index , 'value'],
+      '#error_no_message' => TRUE,
     ];
     $row['condition']['pattern'] = [
       '#type' => 'container',
@@ -341,6 +521,7 @@ protected static function buildOperations($table_id, $row_index, array $ajax_set
     ];
     $operations['add'] = [
       '#type' => 'image_button',
+      '#title' => t('Add'),
       '#src' => drupal_get_path('module', 'webform') . '/images/icons/plus.svg',
       '#limit_validation_errors' => [],
       '#submit' => [[get_called_class(), 'addConditionSubmit']],
@@ -350,6 +531,7 @@ protected static function buildOperations($table_id, $row_index, array $ajax_set
     ];
     $operations['remove'] = [
       '#type' => 'image_button',
+      '#title' => t('Remove'),
       '#src' => drupal_get_path('module', 'webform') . '/images/icons/ex.svg',
       '#limit_validation_errors' => [],
       '#submit' => [[get_called_class(), 'removeRowSubmit']],
@@ -365,7 +547,7 @@ protected static function buildOperations($table_id, $row_index, array $ajax_set
   /****************************************************************************/
 
   /**
-   * Webform submission handler for adding another state.
+   * Form submission handler for adding another state.
    *
    * @param array $form
    *   An associative array containing the structure of the form.
@@ -375,7 +557,7 @@ protected static function buildOperations($table_id, $row_index, array $ajax_set
   public static function addStateSubmit(array &$form, FormStateInterface $form_state) {
     // Get the webform states element by going up one level.
     $button = $form_state->getTriggeringElement();
-    $element =& NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
+    $element =& NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
 
     $values = $element['states']['#value'];
 
@@ -397,12 +579,12 @@ public static function addStateSubmit(array &$form, FormStateInterface $form_sta
     // Update the number of rows.
     $form_state->set(static::getStorageKey($element, 'number_of_rows'), count($values));
 
-    // Rebuild the webform.
+    // Rebuild the form.
     $form_state->setRebuild();
   }
 
   /**
-   * Webform submission handler for adding another condition.
+   * Form submission handler for adding another condition.
    *
    * @param array $form
    *   An associative array containing the structure of the form.
@@ -435,12 +617,12 @@ public static function addConditionSubmit(array &$form, FormStateInterface $form
     // Update the number of rows.
     $form_state->set(static::getStorageKey($element, 'number_of_rows'), count($values));
 
-    // Rebuild the webform.
+    // Rebuild the form.
     $form_state->setRebuild();
   }
 
   /**
-   * Webform submission handler for removing a state or condition.
+   * Form submission handler for removing a state or condition.
    *
    * @param array $form
    *   An associative array containing the structure of the form.
@@ -476,7 +658,32 @@ public static function removeRowSubmit(array &$form, FormStateInterface $form_st
     // Update the number of rows.
     $form_state->set(static::getStorageKey($element, 'number_of_rows'), count($values));
 
-    // Rebuild the webform.
+    // Rebuild the form.
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Form submission handler for editing source.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public static function editSourceSubmit(array &$form, FormStateInterface $form_state) {
+    // Get the webform states element by going up one level.
+    $button = $form_state->getTriggeringElement();
+    $element =& NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));
+
+    // Set edit source.
+    $form_state->set(static::getStorageKey($element, 'edit_source'), TRUE);
+
+    // Convert states to editable string.
+    $value = $element['#value'] ? Yaml::encode($element['#value']) : '';
+    $form_state->setValueForElement($element['states'], $value);
+    NestedArray::setValue($form_state->getUserInput(), $element['states']['#parents'], $value);
+
+    // Rebuild the form.
     $form_state->setRebuild();
   }
 
@@ -485,9 +692,9 @@ public static function removeRowSubmit(array &$form, FormStateInterface $form_st
    */
   public static function ajaxCallback(array &$form, FormStateInterface $form_state) {
     $button = $form_state->getTriggeringElement();
-    $parent_length = (isset($button['#row_index'])) ? -4 : -1;
+    $parent_length = (isset($button['#row_index'])) ? -4 : -2;
     $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, $parent_length));
-    return $element['states'];
+    return $element;
   }
 
   /**
@@ -498,7 +705,11 @@ public static function validateWebformElementStates(&$element, FormStateInterfac
       $states = Yaml::decode($element['states']['#value']);
     }
     else {
-      $states = static::convertFormValuesToFormApiStates($element['states']['#value']);
+      $errors = [];
+      $states = static::convertElementValueToFormApiStates($element, $errors);
+      if ($errors) {
+        $form_state->setError($element, reset($errors));
+      }
     }
     $form_state->setValueForElement($element, NULL);
 
@@ -587,22 +798,30 @@ protected static function getStatesArrayCondition($selector, array $condition) {
   }
 
   /**
-   * Convert states array to Form API #states.
+   * Convert an element's submitted value to Form API #states.
    *
-   * @param array $states_array
-   *   An associative array containing states.
+   * @param array $element
+   *   The form element.
+   * @param array $errors
+   *   An array used to capture errors.
    *
    * @return array
    *   An associative array of states.
    */
-  protected static function convertStatesArrayToFormApiStates(array $states_array = []) {
+  protected static function convertElementValueToFormApiStates(array $element, array &$errors = []) {
     $states = [];
+    $states_array = static::convertFormValuesToStatesArray($element['states']['#value']);
     foreach ($states_array as $state_array) {
       $state = $state_array['state'];
       if (!$state) {
         continue;
       }
 
+      // Check for duplicate states.
+      if (isset($states[$state])) {
+        static::setFormApiStateError($element, $errors, $state);
+      }
+
       // Define values extracted from
       // WebformElementStates::getFormApiStatesCondition().
       $selector = NULL;
@@ -614,6 +833,10 @@ protected static function convertStatesArrayToFormApiStates(array $states_array
       if (count($conditions) === 1) {
         $condition = reset($conditions);
         extract(static::getFormApiStatesCondition($condition));
+        // Check for duplicate selectors.
+        if (isset($states[$state][$selector])) {
+          static::setFormApiStateError($element, $errors, $state, $selector);
+        }
         $states[$state][$selector][$trigger] = $value;
       }
       else {
@@ -631,6 +854,10 @@ protected static function convertStatesArrayToFormApiStates(array $states_array
               ];
             }
             else {
+              // Check for duplicate selectors.
+              if (isset($states[$state][$selector])) {
+                static::setFormApiStateError($element, $errors, $state, $selector);
+              }
               $states[$state][$selector] = [
                 $trigger => $value,
               ];
@@ -642,6 +869,37 @@ protected static function convertStatesArrayToFormApiStates(array $states_array
     return $states;
   }
 
+  /**
+   * Set Form API state error.
+   *
+   * @param array $element
+   *   The form element.
+   * @param array $errors
+   *   An array used to capture errors.
+   * @param null|string $state
+   *   An element state.
+   * @param null|string $selector
+   *   An element selector.
+   */
+  protected static function setFormApiStateError(array $element, array &$errors, $state = NULL, $selector = NULL) {
+    $state_options = OptGroup::flattenOptions($element['#state_options']);
+    $selector_options = OptGroup::flattenOptions($element['#selector_options']);
+
+    if ($state && !$selector) {
+      $t_args = [
+        '%state' => $state_options[$state],
+      ];
+      $errors[] = t('The %state state is declared more than once. There can only be one declaration per state.', $t_args);
+    }
+    elseif ($state && $selector) {
+      $t_args = [
+        '%selector' => $selector_options[$selector],
+        '%state' => $state_options[$state],
+      ];
+      $errors[] = t('The %selector element is used more than once within the %state state. To use multiple values within a trigger try using the pattern trigger.', $t_args);
+    }
+  }
+
   /**
    * Get FAPI states array condition.
    *
@@ -685,7 +943,7 @@ protected static function getFormApiStatesCondition(array $condition) {
    * @return array
    *   An associative array of states.
    */
-  public static function convertFormValuesToStatesArray(array $values = []) {
+  protected static function convertFormValuesToStatesArray(array $values = []) {
     $index = 0;
 
     $states = [];
@@ -705,20 +963,6 @@ public static function convertFormValuesToStatesArray(array $values = []) {
     return $states;
   }
 
-  /**
-   * Convert webform values to states array.
-   *
-   * @param array $values
-   *   Submitted webform values to converted to states array.
-   *
-   * @return array
-   *   An associative array of states.
-   */
-  public static function convertFormValuesToFormApiStates(array $values = []) {
-    $values = static::convertFormValuesToStatesArray($values);
-    return static::convertStatesArrayToFormApiStates($values);
-  }
-
   /**
    * Determine if an element's #states array is customized.
    *
@@ -728,7 +972,7 @@ public static function convertFormValuesToFormApiStates(array $values = []) {
    * @return bool|string
    *   FALSE if #states array is not customized or a warning message.
    */
-  public static function isDefaultValueCustomizedFormApiStates(array $element) {
+  protected static function isDefaultValueCustomizedFormApiStates(array $element) {
     // Empty default values are not customized.
     if (empty($element['#default_value'])) {
       return FALSE;
@@ -763,12 +1007,12 @@ public static function isDefaultValueCustomizedFormApiStates(array $element) {
         }
         elseif (is_string($condition)) {
           if (!in_array($condition, ['and', 'or', 'xor'])) {
-            return t('Conditional logic (Form API #states) is using the %operator operator.', ['%operator' => Unicode::strtoupper($condition)]);
+            return t('Conditional logic (Form API #states) is using the %operator operator.', ['%operator' => mb_strtoupper($condition)]);
           }
 
           // Make sure the same operator is being used between the conditions.
           if ($operator && $operator != $condition) {
-            return t('Conditional logic (Form API #states) has multiple operators.', ['%operator' => Unicode::strtoupper($condition)]);
+            return t('Conditional logic (Form API #states) has multiple operators.', ['%operator' => mb_strtoupper($condition)]);
           }
 
           // Set the operator.
diff --git a/web/modules/webform/src/Element/WebformEmailConfirm.php b/web/modules/webform/src/Element/WebformEmailConfirm.php
index 124613ede388001ad11d376567268e43ea5ba549..dcf2b0a2a59a25203357ad430e3bc7036bc8ce28 100644
--- a/web/modules/webform/src/Element/WebformEmailConfirm.php
+++ b/web/modules/webform/src/Element/WebformEmailConfirm.php
@@ -2,10 +2,8 @@
 
 namespace Drupal\webform\Element;
 
-use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Render\Element\FormElement;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Render\Element\CompositeFormElementTrait;
 use Drupal\webform\Utility\WebformElementHelper;
 
 /**
@@ -18,7 +16,7 @@
  */
 class WebformEmailConfirm extends FormElement {
 
-  use CompositeFormElementTrait;
+  use WebformCompositeFormElementTrait;
 
   /**
    * {@inheritdoc}
@@ -31,11 +29,10 @@ public function getInfo() {
       '#process' => [
         [$class, 'processWebformEmailConfirm'],
       ],
-      '#theme_wrappers' => ['form_element'],
+      '#pre_render' => [
+        [$class, 'preRenderWebformCompositeFormElement'],
+      ],
       '#required' => FALSE,
-      // Add '#markup' property to add an 'id' attribute to the form element.
-      // @see template_preprocess_form_element()
-      '#markup' => '',
     ];
   }
 
@@ -98,7 +95,6 @@ public static function processWebformEmailConfirm(&$element, FormStateInterface
     $element['mail_2']['#value'] = empty($element['#value']) ? NULL : $element['#value']['mail_2'];
     $element['mail_2']['#error_no_message'] = TRUE;
 
-
     // Don't require the main element.
     $element['#required'] = FALSE;
 
@@ -122,13 +118,12 @@ public static function processWebformEmailConfirm(&$element, FormStateInterface
    * Validates an email confirm element.
    */
   public static function validateWebformEmailConfirm(&$element, FormStateInterface $form_state, &$complete_form) {
-
     $mail_1 = trim($element['mail_1']['#value']);
     $mail_2 = trim($element['mail_2']['#value']);
     $has_access = (!isset($element['#access']) || $element['#access'] === TRUE);
     if ($has_access) {
       if ((!empty($mail_1) || !empty($mail_2)) && strcmp($mail_1, $mail_2)) {
-        $form_state->setError($element['mail_2'], t('The specified email addresses do not match.'));
+        $form_state->setError($element, t('The specified email addresses do not match.'));
       }
       else {
         // NOTE: Only mail_1 needs to be validated since mail_2 is the same value.
@@ -138,11 +133,11 @@ public static function validateWebformEmailConfirm(&$element, FormStateInterface
           WebformElementHelper::setRequiredError($element, $form_state, $required_error_title);
         }
         // Verify that the value is not longer than #maxlength.
-        if (isset($element['mail_1']['#maxlength']) && Unicode::strlen($mail_1) > $element['mail_1']['#maxlength']) {
+        if (isset($element['mail_1']['#maxlength']) && mb_strlen($mail_1) > $element['mail_1']['#maxlength']) {
           $t_args = [
             '@name' => $element['mail_1']['#title'],
             '%max' => $element['mail_1']['#maxlength'],
-            '%length' => Unicode::strlen($mail_1),
+            '%length' => mb_strlen($mail_1),
           ];
           $form_state->setError($element, t('@name cannot be longer than %max characters but is currently %length characters long.', $t_args));
         }
diff --git a/web/modules/webform/src/Element/WebformEntityTrait.php b/web/modules/webform/src/Element/WebformEntityTrait.php
index 77d2ddd3299a10ff9abeeaaa7a1b717bbd9def72..6eecd274cdceb822d76ce2844f07d3fb1ca564f3 100644
--- a/web/modules/webform/src/Element/WebformEntityTrait.php
+++ b/web/modules/webform/src/Element/WebformEntityTrait.php
@@ -26,21 +26,32 @@ public function getInfo() {
    *
    * @param array $element
    *   An element.
+   * @param array $settings
+   *   An array of settings used to limit and randomize options.
    *
    * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
    *   Thrown when the current user doesn't have access to the specified entity.
    *
    * @see \Drupal\system\Controller\EntityAutocompleteController
    */
-  public static function setOptions(array &$element) {
+  public static function setOptions(array &$element, array $settings = []) {
     if (!empty($element['#options'])) {
       return;
     }
 
+    // Make sure #target_type is not empty.
+    if (empty($element['#target_type'])) {
+      $element['#options'] = [];
+      return;
+    }
+
     $selection_handler_options = [
       'target_type' => $element['#target_type'],
       'handler' => $element['#selection_handler'],
       'handler_settings' => (isset($element['#selection_settings'])) ? $element['#selection_settings'] : [],
+      // Set '_webform_settings' used to limit and randomize options.
+      // @see webform_query_entity_reference_alter()
+      '_webform_settings' => $settings,
     ];
 
     /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager */
@@ -55,7 +66,11 @@ public static function setOptions(array &$element) {
       $options += $bundle_options;
     }
 
-    $options = self::translateOptions($options, $element);
+    // If the selection handler is not using views, then translate
+    // the entity reference's options.
+    if ($element['#selection_handler'] != 'views') {
+      $options = self::translateOptions($options, $element);
+    }
 
     // Only select menu can support optgroups.
     if ($element['#type'] !== 'webform_entity_select') {
diff --git a/web/modules/webform/src/Element/WebformExcludedBase.php b/web/modules/webform/src/Element/WebformExcludedBase.php
index c3bfb31833a79348e5341a5048c77853fae41f49..b882fca5d3e41a66c92faeab2517136ffa6b7abe 100644
--- a/web/modules/webform/src/Element/WebformExcludedBase.php
+++ b/web/modules/webform/src/Element/WebformExcludedBase.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element\FormElement;
+use Drupal\webform\Plugin\WebformElement\TableSelect;
 
 /**
  * Provides a base webform element for webform excluded elements and columns.
@@ -51,6 +52,8 @@ public static function processWebformExcluded(&$element, FormStateInterface $for
       '#empty' => t('No elements are available.'),
       '#default_value' => array_combine($default_value, $default_value),
     ];
+    TableSelect::setProcessTableSelectCallback($element['tableselect']);
+
     if (isset($element['#parents'])) {
       $element['tableselect']['#parents'] = array_merge($element['#parents'], ['tableselect']);
     }
diff --git a/web/modules/webform/src/Element/WebformExcludedColumns.php b/web/modules/webform/src/Element/WebformExcludedColumns.php
index afc42bc120686b2940525922eccf8d003ae76048..e34b308b5e291ea31b4a3898051fbf4d355707d7 100644
--- a/web/modules/webform/src/Element/WebformExcludedColumns.php
+++ b/web/modules/webform/src/Element/WebformExcludedColumns.php
@@ -43,6 +43,12 @@ public static function getWebformExcludedOptions(array $element) {
       ];
     }
     $elements = $webform->getElementsInitializedFlattenedAndHasValue('view');
+
+    // Replace tokens which can be used in an element's #title.
+    /** @var \Drupal\webform\WebformTokenManagerInterface $token_manager */
+    $token_manager = \Drupal::service('webform.token_manager');
+    $elements = $token_manager->replace($elements, $webform);
+
     foreach ($elements as $key => $element) {
       $options[$key] = [
         'title' => $element['#admin_title'] ?:$element['#title'] ?: $key,
diff --git a/web/modules/webform/src/Element/WebformHelp.php b/web/modules/webform/src/Element/WebformHelp.php
index 927ecd4a74c2e0f5d54db2b3d1cbd41934573b0d..cc13e7b0818dcc6f43d6b335359b7c614e8e8cd9 100644
--- a/web/modules/webform/src/Element/WebformHelp.php
+++ b/web/modules/webform/src/Element/WebformHelp.php
@@ -17,7 +17,9 @@ class WebformHelp extends RenderElement {
   public function getInfo() {
     return [
       '#help' => '',
+      '#help_title' => '',
       '#theme' => 'webform_element_help',
+      '#attributes' => [],
     ];
   }
 
diff --git a/web/modules/webform/src/Element/WebformHtmlEditor.php b/web/modules/webform/src/Element/WebformHtmlEditor.php
index 1cbd804e0eec10316487911b96e62eb3a1224989..8708093087d9272d0201c61ed726402aa42ed497 100644
--- a/web/modules/webform/src/Element/WebformHtmlEditor.php
+++ b/web/modules/webform/src/Element/WebformHtmlEditor.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element\FormElement;
 use Drupal\webform\Utility\WebformElementHelper;
+use Drupal\webform\Utility\WebformXss;
 
 /**
  * Provides a webform element for entering HTML using CodeMirror, TextFormat, or custom CKEditor.
@@ -69,25 +70,20 @@ public static function processWebformHtmlEditor(array $element) {
     // Define value element.
     $element += ['value' => []];
 
-    // Set value element title and hide it.
-    $element['value']['#title'] = $element['#title'];
-    $element['value']['#title_display'] = 'invisible';
+    // Copy properties to value element.
+    $properties = ['#title', '#required', '#attributes', '#default_value'];
+    $element['value'] += array_intersect_key($element, array_combine($properties, $properties));
 
-    // Set value element required.
-    if (isset($element['#required'])) {
-      $element['value']['#required'] = $element['#required'];
-    }
+    // Hide title.
+    $element['value']['#title_display'] = 'invisible';
 
     // Don't display inline form error messages.
     $element['#error_no_message'] = TRUE;
 
-    // Set value element default value.
-    $element['value']['#default_value'] = $element['#default_value'];
-
     // Add validate callback.
     $element += ['#element_validate' => []];
     array_unshift($element['#element_validate'], [get_called_class(), 'validateWebformHtmlEditor']);
-    
+
     // If HTML disabled and no #format is specified return simple CodeMirror
     // HTML editor.
     $disabled = \Drupal::config('webform.settings')->get('html_editor.disabled') ?: ($element['#format'] === FALSE);
@@ -99,9 +95,9 @@ public static function processWebformHtmlEditor(array $element) {
       return $element;
     }
 
-    // If #format or 'webform.settings.html_editor.format' is defined return
+    // If #format or 'webform.settings.html_editor.element_format' is defined return
     // a 'text_format' element.
-    $format = $element['#format'] ?: \Drupal::config('webform.settings')->get('html_editor.format');
+    $format = $element['#format'] ?: \Drupal::config('webform.settings')->get('html_editor.element_format');
     if ($format) {
       $element['value'] += [
         '#type' => 'text_format',
@@ -115,8 +111,8 @@ public static function processWebformHtmlEditor(array $element) {
     // Else use textarea with completely custom HTML Editor.
     $element['value'] += [
       '#type' => 'textarea',
-      '#attributes' => ['class' => ['js-html-editor']],
     ];
+    $element['value']['#attributes']['class'][] = 'js-html-editor';
 
     $element['#attached']['library'][] = 'webform/webform.element.html_editor';
     $element['#attached']['drupalSettings']['webform']['html_editor']['allowedContent'] = static::getAllowedContent();
@@ -207,18 +203,10 @@ public static function getAllowedTags() {
     $allowed_tags = \Drupal::config('webform.settings')->get('element.allowed_tags');
     switch ($allowed_tags) {
       case 'admin':
-        $allowed_tags = Xss::getAdminTagList();
-        // <label>, <fieldset>, <legend>, <font> is missing from allowed tags.
-        $allowed_tags[] = 'label';
-        $allowed_tags[] = 'fieldset';
-        $allowed_tags[] = 'legend';
-        $allowed_tags[] = 'font';
-        return $allowed_tags;
+        return WebformXss::getAdminTagList();
 
       case 'html':
-        $allowed_tags = Xss::getHtmlTagList();
-        $allowed_tags[] = 'font';
-        return $allowed_tags;
+        return WebformXss::getHtmlTagList();
 
       default:
         return preg_split('/ +/', $allowed_tags);
@@ -230,23 +218,28 @@ public static function getAllowedTags() {
    *
    * @param string $text
    *   The text to be filtered.
+   * @param array $options
+   *   HTML markup options.
    *
    * @return array
    *   Render array containing 'processed_text'.
    *
    * @see \Drupal\webform\Plugin\WebformHandler\EmailWebformHandler::getMessage
    */
-  public static function checkMarkup($text) {
+  public static function checkMarkup($text, array $options = []) {
+    $options += [
+      'tidy' => \Drupal::config('webform.settings')->get('html_editor.tidy'),
+    ];
     // Remove <p> tags around a single line of text, which creates minor
     // margin issues.
-    if (\Drupal::config('webform.settings')->get('html_editor.tidy')) {
+    if ($options['tidy']) {
       if (substr_count($text, '<p>') === 1 && preg_match('#^\s*<p>.*</p>\s*$#m', $text)) {
         $text = preg_replace('#^\s*<p>#', '', $text);
         $text = preg_replace('#</p>\s*$#', '', $text);
       }
     }
 
-    if ($format = \Drupal::config('webform.settings')->get('html_editor.format')) {
+    if ($format = \Drupal::config('webform.settings')->get('html_editor.element_format')) {
       return [
         '#type' => 'processed_text',
         '#text' => $text,
@@ -255,6 +248,7 @@ public static function checkMarkup($text) {
     }
     else {
       return [
+        '#theme' => 'webform_html_editor_markup',
         '#markup' => $text,
         '#allowed_tags' => static::getAllowedTags(),
       ];
diff --git a/web/modules/webform/src/Element/WebformImageResolution.php b/web/modules/webform/src/Element/WebformImageResolution.php
new file mode 100644
index 0000000000000000000000000000000000000000..f9c32df581ef82d7ca55aa5a31bf4c3fdf4defd8
--- /dev/null
+++ b/web/modules/webform/src/Element/WebformImageResolution.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Drupal\webform\Element;
+
+use Drupal\Core\Render\Element\FormElement;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element\CompositeFormElementTrait;
+
+/**
+ * Provides a webform image resolution element .
+ *
+ * @FormElement("webform_image_resolution")
+ */
+class WebformImageResolution extends FormElement {
+
+  use CompositeFormElementTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return [
+      '#input' => TRUE,
+      '#size' => 60,
+      '#process' => [
+        [$class, 'processWebformImageResolution'],
+      ],
+      '#theme_wrappers' => ['form_element'],
+      '#required' => FALSE,
+      // Add '#markup' property to add an 'id' attribute to the form element.
+      // @see template_preprocess_form_element()
+      '#markup' => '',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
+    if ($input === FALSE) {
+      if (!isset($element['#default_value'])) {
+        $element['#default_value'] = '';
+      }
+      $max_resolution = explode('x', $element['#default_value']) + ['', ''];
+
+      return [
+        'x' => $max_resolution[0],
+        'y' => $max_resolution[1],
+      ];
+    }
+    else {
+      return $input;
+    }
+  }
+
+  /**
+   * Expand a image resolution field into width and height elements.
+   *
+   * @see \Drupal\image\Plugin\Field\FieldType\ImageItem::fieldSettingsForm
+   */
+  public static function processWebformImageResolution(&$element, FormStateInterface $form_state, &$complete_form) {
+    $element['#tree'] = TRUE;
+
+    $element['#type'] = 'item';
+    $element += [
+      '#description' => t('The maximum allowed image size expressed as WIDTH×HEIGHT (e.g. 640×480). Leave blank for no restriction. If a larger image is uploaded, it will be resized to reflect the given width and height. Resizing images on upload will cause the loss of <a href="http://wikipedia.org/wiki/Exchangeable_image_file_format">EXIF data</a> in the image.'),
+      '#height_title' => t('Maximum height'),
+      '#width_title' => t('Maximum width'),
+      '#field_prefix' => '<div class="container-inline">',
+      '#field_suffix' => '</div>',
+    ];
+    $element['x'] = [
+      '#type' => 'number',
+      '#title' => $element['#width_title'],
+      '#title_display' => 'invisible',
+      '#value' => empty($element['#value']) ? NULL : $element['#value']['x'],
+      '#min' => 1,
+      '#field_suffix' => ' × ',
+    ];
+    $element['y'] = [
+      '#type' => 'number',
+      '#title' => $element['#height_title'],
+      '#title_display' => 'invisible',
+      '#value' => empty($element['#value']) ? NULL : $element['#value']['y'],
+      '#min' => 1,
+      '#field_suffix' => ' ' . t('pixels'),
+    ];
+
+    // Add validate callback.
+    $element += ['#element_validate' => []];
+    array_unshift($element['#element_validate'], [get_called_class(), 'validateWebformImageResolution']);
+
+    return $element;
+  }
+
+  /**
+   * Validates an image resolution element.
+   *
+   * @see \Drupal\image\Plugin\Field\FieldType\ImageItem::validateResolution
+   */
+  public static function validateWebformImageResolution(&$element, FormStateInterface $form_state, &$complete_form) {
+    if (!empty($element['x']['#value']) || !empty($element['y']['#value'])) {
+      foreach (['x', 'y'] as $dimension) {
+        if (!$element[$dimension]['#value']) {
+          // We expect the field name placeholder value to be wrapped in t()
+          // here, so it won't be escaped again as it's already marked safe.
+          $form_state->setError($element[$dimension], t('Both a height and width value must be specified in the @name field.', ['@name' => $element['#title']]));
+          return;
+        }
+      }
+      $form_state->setValueForElement($element, $element['x']['#value'] . 'x' . $element['y']['#value']);
+    }
+    else {
+      $form_state->setValueForElement($element, '');
+    }
+  }
+
+}
diff --git a/web/modules/webform/src/Element/WebformLikert.php b/web/modules/webform/src/Element/WebformLikert.php
index 062a1622ad41bfa167adc30ebbe738a88cfb8b56..c319a97f9d2e9d6d3b3c3c92ab4d13d4d752ac81 100644
--- a/web/modules/webform/src/Element/WebformLikert.php
+++ b/web/modules/webform/src/Element/WebformLikert.php
@@ -3,9 +3,11 @@
 namespace Drupal\webform\Element;
 
 use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Component\Utility\Html;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element\FormElement;
-use Drupal\webform\Utility\WebformArrayHelper;
+use Drupal\webform\Utility\WebformAccessibilityHelper;
+use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\Utility\WebformOptionsHelper;
 
 /**
@@ -28,9 +30,10 @@ public function getInfo() {
       ],
       '#theme_wrappers' => ['form_element'],
       '#required' => FALSE,
+      '#sticky' => TRUE,
       '#questions' => [],
       '#questions_description_display' => 'description',
-      // Using #answers insteads of #options to prevent triggering
+      // Using #answers instead of #options to prevent triggering
       // \Drupal\Core\Form\FormValidator::performRequiredValidation().
       '#answers' => [],
       '#answers_description_display' => 'description',
@@ -47,6 +50,9 @@ public static function processWebformLikert(&$element, FormStateInterface $form_
     // Get answer with optional N/A.
     static::processWebformLikertAnswers($element);
 
+    // Remove 'for' from element's label.
+    $element['#label_attributes']['webform-remove-for-attribute'] = TRUE;
+
     // Process answers.
     $answers = [];
     foreach ($element['#answers'] as $answer_key => $answer) {
@@ -61,7 +67,7 @@ public static function processWebformLikert(&$element, FormStateInterface $form_
         list($answer_title, $answer_description) = explode(WebformOptionsHelper::DESCRIPTION_DELIMITER, $answer);
       }
       $answers[$answer_key] = [
-        'description_property_name' => $answer_description_property_name ,
+        'description_property_name' => $answer_description_property_name,
         'title' => $answer_title,
         'description' => $answer_description,
       ];
@@ -69,7 +75,11 @@ public static function processWebformLikert(&$element, FormStateInterface $form_
 
     // Build header.
     $header = [
-      'likert_question' => ['question' => FALSE],
+      'likert_question' => [
+        'data' => [
+          'title' => WebformAccessibilityHelper::buildVisuallyHidden(t('Questions')),
+        ],
+      ],
     ];
     foreach ($answers as $answer_key => $answer) {
       $header[$answer_key] = [
@@ -82,6 +92,7 @@ public static function processWebformLikert(&$element, FormStateInterface $form_
           $header[$answer_key]['data']['help'] = [
             '#type' => 'webform_help',
             '#help' => $answer['description'],
+            '#help_title' => $answer['title'],
           ];
           break;
 
@@ -97,7 +108,7 @@ public static function processWebformLikert(&$element, FormStateInterface $form_
 
     // Randomize questions.
     if (!empty($element['#questions_randomize'])) {
-      $element['#questions'] = WebformArrayHelper::shuffle($element['#questions']);
+      $element['#questions'] = WebformElementHelper::randomize($element['#questions']);
     }
 
     // Build rows.
@@ -115,16 +126,25 @@ public static function processWebformLikert(&$element, FormStateInterface $form_
       }
 
       $value = (isset($element['#value'][$question_key])) ? $element['#value'][$question_key] : NULL;
+
+      // Get question id.
+      // @see \Drupal\Core\Form\FormBuilder::doBuildForm
+      $question_id = 'edit-' . implode('-', array_merge($element['#parents'], ['table', $question_key, 'likert_question']));
+      $question_id = Html::getUniqueId($question_id);
+
       $row = [];
       // Must format the label as an item so that inline webform errors will be
       // displayed.
       $row['likert_question'] = [
         '#type' => 'item',
         '#title' => $question_title,
+        '#id' => $question_id,
         // Must include an empty <span> so that the item's value is
         // not required.
         '#value' => '<span></span>',
+        '#webform_element' => TRUE,
         '#required' => $element['#required'],
+        '#label_attributes' => ['webform-remove-for-attribute' => TRUE],
       ];
       if ($question_description_property_name) {
         $row['likert_question'][$question_description_property_name] = $question_description;
@@ -141,10 +161,15 @@ public static function processWebformLikert(&$element, FormStateInterface $form_
           // value is NULL.
           // @see \Drupal\Core\Render\Element\Radio::preRenderRadio
           '#value' => ($value === NULL) ? FALSE : (string) $value,
+          '#attributes' => ['aria-labelledby' => $question_id],
         ];
 
-        // Wrap title in span.webform-likert-label so that it can hidden when
-        // Likert is displayed in grid on desktop.
+        // Wrap title in span.webform-likert-label.visually-hidden
+        // so that it can hidden but accessible to screen readers
+        // when Likert is displayed in grid on desktop.
+        // Wrap help and description in
+        // span.webform-likert-(help|description).hidden to block screen
+        // readers except on mobile.
         // @see webform.element.likert.css
         $row[$answer_key]['#title_display'] = 'after';
 
@@ -155,8 +180,11 @@ public static function processWebformLikert(&$element, FormStateInterface $form_
               'help' => [
                 '#type' => 'webform_help',
                 '#help' => $answer['description'],
+                '#help_title' => $answer['title'],
+                '#prefix' => '<span class="webform-likert-help hidden">',
+                '#suffix' => '</span>',
               ],
-              '#prefix' => '<span class="webform-likert-label">',
+              '#prefix' => '<span class="webform-likert-label visually-hidden">',
               '#suffix' => '</span>',
             ];
             $row[$answer_key]['#title'] = \Drupal::service('renderer')->render($build);
@@ -164,14 +192,14 @@ public static function processWebformLikert(&$element, FormStateInterface $form_
 
           case 'description':
             $row[$answer_key] += [
-              '#title' => new FormattableMarkup('<span class="webform-likert-label">@title</span>', ['@title' => $answer['title']]),
-              '#description' => new FormattableMarkup('<span class="webform-likert-description">@description</span>', ['@description' => $answer['description']]),
+              '#title' => new FormattableMarkup('<span class="webform-likert-label visually-hidden">@title</span>', ['@title' => $answer['title']]),
+              '#description' => new FormattableMarkup('<span class="webform-likert-description hidden">@description</span>', ['@description' => $answer['description']]),
             ];
             break;
 
           default:
             $row[$answer_key] += [
-              '#title' => new FormattableMarkup('<span class="webform-likert-label">@title</span>', ['@title' => $answer['title']]),
+              '#title' => new FormattableMarkup('<span class="webform-likert-label visually-hidden">@title</span>', ['@title' => $answer['title']]),
             ];
         }
       }
@@ -181,10 +209,13 @@ public static function processWebformLikert(&$element, FormStateInterface $form_
     $element['table'] = [
       '#type' => 'table',
       '#header' => $header,
+      '#sticky' => $element['#sticky'],
       '#attributes' => [
         'class' => ['webform-likert-table'],
         'data-likert-answers-count' => count($element['#answers']),
       ],
+      '#prefix' => '<div class="webform-likert-table-wrapper">',
+      '#suffix' => '</div>',
     ] + $rows;
 
     // Build table element with selected properties.
diff --git a/web/modules/webform/src/Element/WebformLocation.php b/web/modules/webform/src/Element/WebformLocationBase.php
similarity index 54%
rename from web/modules/webform/src/Element/WebformLocation.php
rename to web/modules/webform/src/Element/WebformLocationBase.php
index 4ffdfb5a1161cfbf63549b0a697270075ad5eed3..13843d80f6f90b882eb5092582ff240823076366 100644
--- a/web/modules/webform/src/Element/WebformLocation.php
+++ b/web/modules/webform/src/Element/WebformLocationBase.php
@@ -5,11 +5,16 @@
 use Drupal\Core\Form\FormStateInterface;
 
 /**
- * Provides a webform element for a location element.
- *
- * @FormElement("webform_location")
+ * Provides a webform base element for a location element.
  */
-class WebformLocation extends WebformCompositeBase {
+abstract class WebformLocationBase extends WebformCompositeBase {
+
+  /**
+   * The location element's class name.
+   *
+   * @var string
+   */
+  protected static $name;
 
   /**
    * {@inheritdoc}
@@ -17,91 +22,62 @@ class WebformLocation extends WebformCompositeBase {
   public function getInfo() {
     return parent::getInfo() + [
       '#theme' => 'webform_composite_location',
-      '#api_key' => '',
-      '#hidden' => FALSE,
-      '#geolocation' => FALSE,
       '#map' => FALSE,
+      '#geolocation' => FALSE,
+      '#hidden' => FALSE,
     ];
   }
 
+  /**
+   * Get location attributes.
+   *
+   * @return array
+   *   An associative array container location attribute name and titles.
+   */
+  public static function getLocationAttributes() {
+    return [];
+  }
+
   /**
    * {@inheritdoc}
    */
   public static function getCompositeElements(array $element) {
-    // @see https://developers.google.com/maps/documentation/javascript/geocoding#GeocodingAddressTypes
-    $attributes = [];
-    $attributes['lat'] = [
-      '#title' => t('Latitude'),
-    ];
-    $attributes['lng'] = [
-      '#title' => t('Longitude'),
-    ];
-    $attributes['location'] = [
-      '#title' => t('Location'),
-    ];
-    $attributes['formatted_address'] = [
-      '#title' => t('Formatted Address'),
-    ];
-    $attributes['street_address'] = [
-      '#title' => t('Street Address'),
-    ];
-    $attributes['street_number'] = [
-      '#title' => t('Street Number'),
-    ];
-    $attributes['subpremise'] = [
-      '#title' => t('Unit'),
-    ];
-    $attributes['postal_code'] = [
-      '#title' => t('Postal Code'),
-    ];
-    $attributes['locality'] = [
-      '#title' => t('Locality'),
-    ];
-    $attributes['sublocality'] = [
-      '#title' => t('City'),
-    ];
-    $attributes['administrative_area_level_1'] = [
-      '#title' => t('State/Province'),
-    ];
-    $attributes['country'] = [
-      '#title' => t('Country'),
-    ];
-    $attributes['country_short'] = [
-      '#title' => t('Country Code'),
-    ];
-
-    foreach ($attributes as $name => &$attribute_element) {
-      $attribute_element['#type'] = 'textfield';
-
-      $attribute_element['#attributes'] = [
-        'data-webform-location-attribute' => $name,
-      ];
-    }
-
     $elements = [];
+
     $elements['value'] = [
       '#type' => 'textfield',
       '#title' => t('Address'),
       '#attributes' => [
-        'class' => ['webform-location-geocomplete'],
+        'class' => ['webform-location-' . static::$name],
       ],
     ];
 
-    $elements += $attributes;
+    $attributes = static::getLocationAttributes();
+    foreach ($attributes as $name => $title) {
+      $elements[$name] = [
+        '#title' => $title,
+        '#type' => 'textfield',
+        '#error_no_message' => TRUE,
+        '#attributes' => [
+          'data-webform-location-' . static::$name . '-attribute' => $name,
+        ],
+      ];
+    }
+
     return $elements;
   }
 
   /**
    * {@inheritdoc}
    */
-  public static function preRenderCompositeFormElement($element) {
-    $element = WebformCompositeBase::preRenderCompositeFormElement($element);
-
+  public static function preRenderWebformCompositeFormElement($element) {
     // Hide location element webform display only if #geolocation is also set.
     if (!empty($element['#hidden']) && !empty($element['#geolocation'])) {
       $element['#wrapper_attributes']['style'] = 'display: none';
     }
 
+    $element = WebformCompositeBase::preRenderWebformCompositeFormElement($element);
+
     return $element;
   }
 
@@ -132,32 +108,29 @@ public static function processWebformComposite(&$element, FormStateInterface $fo
       }
     }
 
-    // Set required.
-    if (isset($element['#required'])) {
-      $element['value']['#required'] = $element['#required'];
-    }
+    // Get shared properties.
+    $shared_properties = [
+      '#required',
+      '#placeholder',
+    ];
+    $element['value'] += array_intersect_key($element, array_combine($shared_properties, $shared_properties));
 
     // Set Geolocation detection attribute.
     if (!empty($element['#geolocation'])) {
-      $element['value']['#attributes']['data-webform-location-geolocation'] = 'data-webform-location-geolocation';
+      $element['value']['#attributes']['data-webform-location-' . static::$name . '-geolocation'] = 'data-webform-location-' . static::$name . '-geolocation';
     }
 
     // Set Map attribute.
     if (!empty($element['#map']) && empty($element['#hidden'])) {
-      $element['value']['#attributes']['data-webform-location-map'] = 'data-webform-location-map';
+      $element['value']['#attributes']['data-webform-location-' . static::$name . '-map'] = 'data-webform-location-' . static::$name . '-map';
     }
 
-    // Add Google Maps API key which is required by
-    // https://maps.googleapis.com/maps/api/js?key=API_KEY&libraries=places
-    // @see webform_js_alter()
-    $api_key = (!empty($element['#api_key'])) ? $element['#api_key'] : \Drupal::config('webform.settings')->get('element.default_google_maps_api_key');
-    $element['#attached']['drupalSettings']['webform']['location']['google_maps_api_key'] = $api_key;
-
-    $element['#attached']['library'][] = 'webform/webform.element.location';
-
     $element += ['#element_validate' => []];
     array_unshift($element['#element_validate'], [get_called_class(), 'validateWebformLocation']);
 
+    // Attach library.
+    $element['#attached']['library'][] = 'webform/webform.element.location.' . static::$name;
+
     return $element;
   }
 
@@ -168,9 +141,9 @@ public static function validateWebformLocation(&$element, FormStateInterface $fo
     $value = $element['#value'];
 
     $has_access = (!isset($element['#access']) || $element['#access'] === TRUE);
-    if ($has_access && !empty($element['#required']) && empty($value['location'])) {
+    if ($has_access && !empty($element['#required']) && empty($value['lat'])) {
       $t_args = ['@title' => !empty($element['#title']) ? $element['#title'] : t('Location')];
-      $form_state->setError($element, t('The @title is not valid.', $t_args));
+      $form_state->setError($element['value'], t('The @title is not valid.', $t_args));
     }
   }
 
diff --git a/web/modules/webform/src/Element/WebformLocationGeocomplete.php b/web/modules/webform/src/Element/WebformLocationGeocomplete.php
new file mode 100644
index 0000000000000000000000000000000000000000..cfef42a9aad468977cec208436537a332911a8fd
--- /dev/null
+++ b/web/modules/webform/src/Element/WebformLocationGeocomplete.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\webform\Element;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a webform element for a location geocomplete element.
+ *
+ * @FormElement("webform_location_geocomplete")
+ */
+class WebformLocationGeocomplete extends WebformLocationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $name = 'geocomplete';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    return parent::getInfo() + [
+      '#api_key' => '',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLocationAttributes() {
+    return [
+      'lat' => t('Latitude'),
+      'lng' => t('Longitude'),
+      'location' => t('Location'),
+      'formatted_address' => t('Formatted Address'),
+      'street_address' => t('Street Address'),
+      'street_number' => t('Street Number'),
+      'subpremise' => t('Unit'),
+      'postal_code' => t('Postal Code'),
+      'locality' => t('Locality'),
+      'sublocality' => t('City'),
+      'administrative_area_level_1' => t('State/Province'),
+      'country' => t('Country'),
+      'country_short' => t('Country Code'),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function processWebformComposite(&$element, FormStateInterface $form_state, &$complete_form) {
+    $element = parent::processWebformComposite($element, $form_state, $complete_form);
+
+    // Add Google Maps API key which is required by
+    // https://maps.googleapis.com/maps/api/js?key=API_KEY&libraries=places
+    // @see webform_js_alter()
+    $api_key = (!empty($element['#api_key'])) ? $element['#api_key'] : \Drupal::config('webform.settings')->get('element.default_google_maps_api_key');
+    $element['#attached']['drupalSettings']['webform']['location']['geocomplete']['api_key'] = $api_key;
+
+    return $element;
+  }
+
+}
diff --git a/web/modules/webform/src/Element/WebformLocationPlaces.php b/web/modules/webform/src/Element/WebformLocationPlaces.php
new file mode 100644
index 0000000000000000000000000000000000000000..ca3e6d1d3134e9d4c1c2e4bfdd70c0f4f2bbdcf0
--- /dev/null
+++ b/web/modules/webform/src/Element/WebformLocationPlaces.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\webform\Element;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a webform element for a location places element.
+ *
+ * @FormElement("webform_location_places")
+ */
+class WebformLocationPlaces extends WebformLocationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $name = 'places';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    return parent::getInfo() + [
+      '#app_id' => '',
+      '#api_key' => '',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLocationAttributes() {
+    return [
+      'lat' => t('Latitude'),
+      'lng' => t('Longitude'),
+      'name' => t('Name'),
+      'city' => t('City'),
+      'country' => t('Country'),
+      'country_code' => t('Country Code'),
+      'administrative' => t('State/Province'),
+      'county' => t('County'),
+      'suburb' => t('Suburb'),
+      'postcode' => t('Postal Code'),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function processWebformComposite(&$element, FormStateInterface $form_state, &$complete_form) {
+    $element = parent::processWebformComposite($element, $form_state, $complete_form);
+
+    // Add Algolia application id and API key.
+    $app_id = (!empty($element['#app_id'])) ? $element['#app_id'] : \Drupal::config('webform.settings')->get('element.default_algolia_places_app_id');
+    $api_key = (!empty($element['#api_key'])) ? $element['#api_key'] : \Drupal::config('webform.settings')->get('element.default_algolia_places_api_key');
+    $element['#attached']['drupalSettings']['webform']['location']['places'] = [
+      'app_id' => $app_id,
+      'api_key' => $api_key,
+    ];
+
+    return $element;
+  }
+
+}
diff --git a/web/modules/webform/src/Element/WebformManagedFileBase.php b/web/modules/webform/src/Element/WebformManagedFileBase.php
index 450c2175e3edc80b2ecaf8ed16b709ed342d08fb..2f8864083eac03e25c2ac6f73339a957a37434f7 100644
--- a/web/modules/webform/src/Element/WebformManagedFileBase.php
+++ b/web/modules/webform/src/Element/WebformManagedFileBase.php
@@ -17,7 +17,7 @@
   abstract class WebformManagedFileBase extends ManagedFile {
 
     /**
-     * The the types of files that the server accepts.
+     * The types of files that the server accepts.
      *
      * @var string
      *
diff --git a/web/modules/webform/src/Element/WebformMapping.php b/web/modules/webform/src/Element/WebformMapping.php
index 302fb18cc81fb15a07d729aba2df998369cca6bb..85c9851764e1b027139dec42acf15494e1592371 100644
--- a/web/modules/webform/src/Element/WebformMapping.php
+++ b/web/modules/webform/src/Element/WebformMapping.php
@@ -60,6 +60,7 @@ public static function processWebformMapping(&$element, FormStateInterface $form
     $destination_element_base = [
       '#title_display' => 'invisible',
       '#required' => ($element['#required'] === self::REQUIRED_ALL) ? TRUE : FALSE,
+      '#error_no_message'  => ($element['#required'] !== self::REQUIRED_ALL) ? TRUE : FALSE,
     ];
 
     // Get base #destination__* properties.
@@ -71,8 +72,8 @@ public static function processWebformMapping(&$element, FormStateInterface $form
 
     // Build header.
     $header = [
-      ['data' => ['#markup' => $element['#source__title'] . ' ' . $arrow], 'width' => '50%'],
-      ['data' => ['#markup' => $element['#destination__title']], 'width' => '50%'],
+      ['data' => ['#markup' => $element['#source__title'] . ' ' . $arrow]],
+      ['data' => ['#markup' => $element['#destination__title']]],
     ];
 
     // Build rows.
@@ -131,6 +132,8 @@ public static function processWebformMapping(&$element, FormStateInterface $form
       webform_process_states($element, '#wrapper_attributes');
     }
 
+    $element['#attached']['library'][] = 'webform/webform.element.mapping';
+
     return $element;
   }
 
diff --git a/web/modules/webform/src/Element/WebformMarkup.php b/web/modules/webform/src/Element/WebformMarkup.php
index 78fd0148f86a027d0508df042a1cbd085760e877..ec5b83cf6f4788cd868fcf0be6e883b6fa490ecd 100644
--- a/web/modules/webform/src/Element/WebformMarkup.php
+++ b/web/modules/webform/src/Element/WebformMarkup.php
@@ -15,7 +15,46 @@ class WebformMarkup extends RenderElement {
    * {@inheritdoc}
    */
   public function getInfo() {
-    return [];
+    $class = get_class($this);
+    return [
+      '#pre_render' => [
+        [$class, 'preRenderWebformMarkup'],
+      ],
+    ];
+  }
+
+  /**
+   * Create webform markup for rendering.
+   *
+   * @param array $element
+   *   An associative array containing the properties and children of the
+   *   element.
+   *
+   * @return array
+   *   The modified element with webform html markup.
+   */
+  public static function preRenderWebformMarkup(array $element) {
+    // Make sure that #markup is defined.
+    if (!isset($element['#markup'])) {
+      return $element;
+    }
+
+    // Replace #markup with renderable webform HTML editor markup.
+    $element['markup'] = WebformHtmlEditor::checkMarkup($element['#markup'], ['tidy' => FALSE]);
+    unset($element['#markup']);
+
+    // Must set wrapper id attribute since we are no longer including #markup.
+    // @see template_preprocess_form_element()
+    if (isset($element['#theme_wrappers']) && !empty($element['#id'])) {
+      $element['#wrapper_attributes']['id'] = $element['#id'];
+    }
+
+    // Sent #name property which is used by form-item-* classes.
+    if (!isset($element['#name']) && isset($element['#webform_key'])) {
+      $element['#name'] = $element['#webform_key'];
+    }
+
+    return $element;
   }
 
 }
diff --git a/web/modules/webform/src/Element/WebformMultiple.php b/web/modules/webform/src/Element/WebformMultiple.php
index 6dedf4aee82ab5106100a881909f1372cfba862a..0ebce0005ac0571828043572a0b00812cb8dd2e8 100644
--- a/web/modules/webform/src/Element/WebformMultiple.php
+++ b/web/modules/webform/src/Element/WebformMultiple.php
@@ -7,6 +7,7 @@
 use Drupal\Core\Render\Element\FormElement;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Template\Attribute;
+use Drupal\webform\Utility\WebformAccessibilityHelper;
 use Drupal\webform\Utility\WebformElementHelper;
 
 /**
@@ -29,20 +30,22 @@ public function getInfo() {
     return [
       '#input' => TRUE,
       '#access' => TRUE,
-      '#label' => t('item'),
-      '#labels' => t('items'),
       '#key' => NULL,
       '#header' => NULL,
       '#element' => [
         '#type' => 'textfield',
         '#title' => t('Item value'),
         '#title_display' => 'invisible',
-        '#placeholder' => t('Enter value'),
+        '#placeholder' => t('Enter value…'),
       ],
       '#cardinality' => FALSE,
-      '#min_items' => 1,
+      '#min_items' => NULL,
+      '#no_items_message' => $this->t('No items entered. Please add items below.'),
       '#empty_items' => 1,
-      '#add_more' => 1,
+      '#add_more' => TRUE,
+      '#add_more_items' => 1,
+      '#add_more_button_label' => $this->t('Add'),
+      '#add_more_input_label' => $this->t('more items'),
       '#sorting' => TRUE,
       '#operations' => TRUE,
       '#add' => TRUE,
@@ -76,7 +79,7 @@ public static function valueCallback(&$element, $input, FormStateInterface $form
       return static::convertValuesToItems($element, $input['items']);
     }
     else {
-      return NULL;
+      return [];
     }
   }
 
@@ -87,9 +90,22 @@ public static function processWebformMultiple(&$element, FormStateInterface $for
     // Set tree.
     $element['#tree'] = TRUE;
 
-    // Disable operation when #cardinality is set.
-    if (!empty($element['#cardinality'])) {
-      $element['#operations'] = FALSE;
+    // Remove 'for' from the element's label.
+    $element['#label_attributes']['webform-remove-for-attribute'] = TRUE;
+
+    // Set min items based on when the element is required.
+    if (!isset($element['#min_items']) || $element['#min_items'] === '') {
+      $element['#min_items'] = (empty($element['#required'])) ? 0 : 1;
+    }
+
+    // Make sure min items does not exceed cardinality.
+    if (!empty($element['#cardinality']) && $element['#min_items'] > $element['#cardinality']) {
+      $element['#min_items'] = $element['#cardinality'];
+    }
+
+    // Make sure empty items does not exceed cardinality.
+    if (!empty($element['#cardinality']) && $element['#empty_items'] > $element['#cardinality']) {
+      $element['#empty_items'] = $element['#cardinality'];
     }
 
     // Add validate callback that extracts the array of items.
@@ -99,40 +115,50 @@ public static function processWebformMultiple(&$element, FormStateInterface $for
     // Wrap this $element in a <div> that handle #states.
     WebformElementHelper::fixStatesWrapper($element);
 
-    if ($element['#cardinality']) {
-      // If the cardinality is set limit number of items to this value.
-      $number_of_items = $element['#cardinality'];
-    }
-    else {
-      // Get unique key used to store the current number of items.
-      $number_of_items_storage_key = static::getStorageKey($element, 'number_of_items');
-
-      // Store the number of items which is the number of
-      // #default_values + number of empty_items.
-      if ($form_state->get($number_of_items_storage_key) === NULL) {
-        if (empty($element['#default_value']) || !is_array($element['#default_value'])) {
-          $number_of_default_values = 0;
-        }
-        else {
-          $number_of_default_values = count($element['#default_value']);
-        }
-        $number_of_empty_items = (int) $element['#empty_items'];
-        $number_of_items = $number_of_default_values + $number_of_empty_items;
+    // Get unique key used to store the current number of items.
+    $number_of_items_storage_key = static::getStorageKey($element, 'number_of_items');
 
-        $min_items = (int) $element['#min_items'];
-        $number_of_items = ($number_of_items < $min_items) ? $min_items : $number_of_items;
+    // Store the number of items which is the number of
+    // #default_values + number of empty_items.
+    if ($form_state->get($number_of_items_storage_key) === NULL) {
+      if (empty($element['#default_value']) || !is_array($element['#default_value'])) {
+        $number_of_default_values = 0;
+      }
+      else {
+        $number_of_default_values = count($element['#default_value']);
+      }
+      $number_of_empty_items = (int) $element['#empty_items'];
+      $number_of_items = $number_of_default_values + $number_of_empty_items;
 
-        if ($number_of_items < 1) {
-          $number_of_items = 1;
-        }
-        $form_state->set($number_of_items_storage_key, $number_of_items);
+      // Make sure number of items is greated than min items.
+      $min_items = (int) $element['#min_items'];
+      $number_of_items = ($number_of_items < $min_items) ? $min_items : $number_of_items;
+
+      // Make sure number of (default) items does not exceed cardinality.
+      if (!empty($element['#cardinality']) && $number_of_items > $element['#cardinality']) {
+        $number_of_items = $element['#cardinality'];
       }
 
-      $number_of_items = $form_state->get($number_of_items_storage_key);
+      $form_state->set($number_of_items_storage_key, $number_of_items);
     }
 
+    $number_of_items = $form_state->get($number_of_items_storage_key);
+
     $table_id = implode('_', $element['#parents']) . '_table';
 
+    // Disable add operation when #cardinality is met
+    // and make sure to limit the number of items.
+    if (!empty($element['#cardinality']) && $number_of_items >= $element['#cardinality']) {
+      $element['#add'] = FALSE;
+      $number_of_items = $element['#cardinality'];
+      $form_state->set($number_of_items_storage_key, $number_of_items);
+    }
+
+    // Add wrapper to the element.
+    $element += ['#prefix' => '', '#suffix' => ''];
+    $element['#prefix'] = '<div id="' . $table_id . '">' . $element['#prefix'];
+    $element['#suffix'] .= '</div>';
+
     // DEBUG:
     // Disable Ajax callback by commenting out the below callback and wrapper.
     $ajax_settings = [
@@ -152,7 +178,6 @@ public static function processWebformMultiple(&$element, FormStateInterface $for
     $weight = 0;
     $rows = [];
 
-
     if (!$form_state->isProcessingInput() && isset($element['#default_value']) && is_array($element['#default_value'])) {
       $default_values = $element['#default_value'];
     }
@@ -185,48 +210,65 @@ public static function processWebformMultiple(&$element, FormStateInterface $for
     }
 
     // Build table.
-    $attributes = ['id' => $table_id, 'class' => ['webform-multiple-table']];
+    $attributes = ['class' => ['webform-multiple-table']];
     if (count($element['#element']) > 1) {
       $attributes['class'][] = 'webform-multiple-table-responsive';
     }
     $element['items'] = [
       '#prefix' => '<div' . new Attribute($attributes) . '>',
       '#suffix' => '</div>',
-      '#type' => 'table',
-      '#header' => $header,
     ] + $rows;
 
-    // Add sorting to table.
-    if ($element['#sorting']) {
-      $element['items']['#tabledrag'] = [
-        [
-          'action' => 'order',
-          'relationship' => 'sibling',
-          'group' => 'webform-multiple-sort-weight',
-        ],
+    // Display table if there are any rows.
+    if ($rows) {
+      $element['items'] += [
+        '#type' => 'table',
+        '#header' => $header,
+      ] + $rows;
+
+      // Add sorting to table.
+      if ($element['#sorting']) {
+        $element['items']['#tabledrag'] = [
+          [
+            'action' => 'order',
+            'relationship' => 'sibling',
+            'group' => 'webform-multiple-sort-weight',
+          ],
+        ];
+      }
+    }
+    elseif (!empty($element['#no_items_message'])) {
+      $element['items'] += [
+        '#type' => 'webform_message',
+        '#message_message' => $element['#no_items_message'],
+        '#message_type' => 'info',
+        '#attributes' => ['class' => ['webform-multiple-table--no-items-message']],
       ];
     }
 
-    // Build add items actions.
-    if (empty($element['#cardinality'])) {
+    // Build add more actions.
+    if ($element['#add_more'] && (empty($element['#cardinality']) || ($number_of_items < $element['#cardinality']))) {
       $element['add'] = [
-        '#prefix' => '<div class="container-inline">',
+        '#prefix' => '<div class="webform-multiple-add js-webform-multiple-add container-inline">',
         '#suffix' => '</div>',
       ];
       $element['add']['submit'] = [
         '#type' => 'submit',
-        '#value' => t('Add'),
+        '#value' => $element['#add_more_button_label'],
         '#limit_validation_errors' => [],
         '#submit' => [[get_called_class(), 'addItemsSubmit']],
         '#ajax' => $ajax_settings,
         '#name' => $table_id . '_add',
       ];
+      $max = ($element['#cardinality']) ? $element['#cardinality'] - $number_of_items : 100;
       $element['add']['more_items'] = [
         '#type' => 'number',
+        '#title' => $element['#add_more_button_label'] . ' ' . $element['#add_more_input_label'],
+        '#title_display' => 'invisible',
         '#min' => 1,
-        '#max' => 100,
-        '#default_value' => $element['#add_more'],
-        '#field_suffix' => t('more @labels', ['@labels' => $element['#labels']]),
+        '#max' => $max,
+        '#default_value' => $element['#add_more_items'],
+        '#field_suffix' => $element['#add_more_input_label'],
         '#error_no_message' => TRUE,
       ];
     }
@@ -244,7 +286,7 @@ public static function processWebformMultiple(&$element, FormStateInterface $for
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The current state of the form.
    * @param array $complete_form
-   *    An associative array containing the structure of the form.
+   *   An associative array containing the structure of the form.
    */
   protected static function initializeElement(array &$element, FormStateInterface $form_state, array &$complete_form) {
     // Track element child keys.
@@ -277,11 +319,11 @@ protected static function initializeElement(array &$element, FormStateInterface
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The current state of the form.
    * @param array $complete_form
-   *    An associative array containing the structure of the form.
+   *   An associative array containing the structure of the form.
    * @param array $sub_elements
    *   The sub element.
    * @param array $required_states
-   *   An associative array of required states froim the main element's
+   *   An associative array of required states from the main element's
    *   visible/hidden states.
    */
   protected static function initializeElementRecursive(array $element, FormStateInterface $form_state, array &$complete_form, array &$sub_elements, array $required_states) {
@@ -360,24 +402,36 @@ protected static function buildElementHeader(array $element) {
 
     if (empty($element['#header'])) {
       return [
-        ['data' => '', 'colspan' => ($colspan + 1)],
+        [
+          'data' => (!empty($element['#title'])) ? WebformAccessibilityHelper::buildVisuallyHidden($element['#title']) : [],
+          'colspan' => ($colspan + 1),
+        ],
       ];
     }
     elseif (is_array($element['#header'])) {
       $header = [];
 
       if ($element['#sorting']) {
-        $header[] = ['class' => ["$table_id--handle", "webform-multiple-table--handle"]];
+        $header[] = [
+          'data' => WebformAccessibilityHelper::buildVisuallyHidden(t('Re-order')),
+          'class' => ["$table_id--handle", 'webform-multiple-table--handle'],
+        ];
       }
 
       $header = array_merge($header, $element['#header']);
 
       if ($element['#sorting']) {
-        $header[] = ['class' => ["$table_id--weight", "webform-multiple-table--weight"]];
+        $header[] = [
+          'data' => ['#markup' => t('Weight')],
+          'class' => ["$table_id--weight", 'webform-multiple-table--weight'],
+        ];
       }
 
       if ($element['#operations']) {
-        $header[] = ['class' => ["$table_id--handle", "webform-multiple-table--operations"]];
+        $header[] = [
+          'data' => WebformAccessibilityHelper::buildVisuallyHidden(t('Operations')),
+          'class' => ["$table_id--handle", 'webform-multiple-table--operations'],
+        ];
       }
 
       return $header;
@@ -391,7 +445,10 @@ protected static function buildElementHeader(array $element) {
       $header = [];
 
       if ($element['#sorting']) {
-        $header['_handle_'] = ['class' => ["$table_id--handle", "webform-multiple-table--handle"]];
+        $header['_handle_'] = [
+          'data' => WebformAccessibilityHelper::buildVisuallyHidden(t('Re-order')),
+          'class' => ["$table_id--handle", "webform-multiple-table--handle"],
+        ];
       }
 
       if ($element['#child_keys']) {
@@ -400,9 +457,11 @@ protected static function buildElementHeader(array $element) {
             continue;
           }
 
+          $child_title = (!empty($element['#element'][$child_key]['#title'])) ? $element['#element'][$child_key]['#title'] : '';
+
           $title = [];
           $title['title'] = [
-            '#markup' => (!empty($element['#element'][$child_key]['#title'])) ? $element['#element'][$child_key]['#title'] : '',
+            '#markup' => $child_title,
           ];
           if (!empty($element['#element'][$child_key]['#required']) || !empty($element['#element'][$child_key]['#_required'])) {
             $title['title'] += [
@@ -414,6 +473,7 @@ protected static function buildElementHeader(array $element) {
             $title['help'] = [
               '#type' => 'webform_help',
               '#help' => $element['#element'][$child_key]['#help'],
+              '#help_title' => $child_title,
             ];
           }
           $header[$child_key] = [
@@ -438,6 +498,7 @@ protected static function buildElementHeader(array $element) {
 
       if ($element['#operations']) {
         $header['_operations_'] = [
+          'data' => WebformAccessibilityHelper::buildVisuallyHidden(t('Operations')),
           'class' => ["$table_id--operations", "webform-multiple-table--operations"],
         ];
       }
@@ -510,11 +571,14 @@ protected static function buildElementRow($table_id, $row_index, array $element,
           if (!isset($element['#access']) || $element['#access'] !== FALSE) {
             $hidden_elements[$child_key]['#type'] = 'hidden';
             // Unset #access, #element_validate, and #pre_render.
-            // @see \Drupal\webform\Plugin\WebformElementBase::prepare().
+            // @see \Drupal\webform\Plugin\WebformElementBase::prepare()
+            // Unset #options to prevent An illegal choice has been detected.
+            // @see \Drupal\Core\Form\FormValidator::performRequiredValidation
             unset(
               $hidden_elements[$child_key]['#access'],
               $hidden_elements[$child_key]['#element_validate'],
-              $hidden_elements[$child_key]['#pre_render']
+              $hidden_elements[$child_key]['#pre_render'],
+              $hidden_elements[$child_key]['#options']
             );
           }
           static::setElementRowParentsRecursive($hidden_elements[$child_key], $child_key, $hidden_parents);
@@ -624,7 +688,7 @@ protected static function isHidden(array $element) {
   }
 
   /**
-   * Set element row default value recusively.
+   * Set element row default value recursively.
    *
    * @param array $element
    *   The element.
@@ -641,7 +705,7 @@ protected static function setElementRowDefaultValueRecursive(array &$element, ar
   }
 
   /**
-   * Set element row default value recusively.
+   * Set element row default value recursively.
    *
    * @param array $element
    *   The element.
@@ -707,14 +771,15 @@ public static function addItemsSubmit(array &$form, FormStateInterface $form_sta
     $form_state->set($number_of_items_storage_key, $number_of_items + $more_items);
 
     // Reset values.
-    $element['items']['#value'] = array_values($element['items']['#value']);
-    $form_state->setValueForElement($element['items'], $element['items']['#value']);
-    NestedArray::setValue($form_state->getUserInput(), $element['items']['#parents'], $element['items']['#value']);
+    $items = (!empty($element['items']['#value'])) ? array_values($element['items']['#value']) : [];
+    $element['items']['#value'] = $items;
+    $form_state->setValueForElement($element['items'], $items);
+    NestedArray::setValue($form_state->getUserInput(), $element['items']['#parents'], $items);
 
     $action_key = static::getStorageKey($element, 'action');
     $form_state->set($action_key, TRUE);
 
-      // Rebuild the webform.
+    // Rebuild the form.
     $form_state->setRebuild();
   }
 
@@ -751,7 +816,7 @@ public static function addItemSubmit(array &$form, FormStateInterface $form_stat
     $action_key = static::getStorageKey($element, 'action');
     $form_state->set($action_key, TRUE);
 
-    // Rebuild the webform.
+    // Rebuild the form.
     $form_state->setRebuild();
   }
 
@@ -775,8 +840,8 @@ public static function removeItemSubmit(array &$form, FormStateInterface $form_s
     // Remove one item from the 'number of items'.
     $number_of_items_storage_key = static::getStorageKey($element, 'number_of_items');
     $number_of_items = $form_state->get($number_of_items_storage_key);
-    // Never allow the number of items to be less than 1.
-    if ($number_of_items != 1) {
+    // Never allow the number of items to be less than #min_items.
+    if ($number_of_items > $element['#min_items']) {
       $form_state->set($number_of_items_storage_key, $number_of_items - 1);
     }
 
@@ -787,7 +852,7 @@ public static function removeItemSubmit(array &$form, FormStateInterface $form_s
     $action_key = static::getStorageKey($element, 'action');
     $form_state->set($action_key, TRUE);
 
-    // Rebuild the webform.
+    // Rebuild the form.
     $form_state->setRebuild();
   }
 
@@ -798,11 +863,11 @@ public static function ajaxCallback(array &$form, FormStateInterface $form_state
     $button = $form_state->getTriggeringElement();
     $parent_length = (isset($button['#row_index'])) ? -4 : -2;
     $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, $parent_length));
-    return $element['items'];
+    return $element;
   }
 
   /**
-   * Validates webform list element.
+   * Validates webform multiple element.
    */
   public static function validateWebformMultiple(&$element, FormStateInterface $form_state, &$complete_form) {
     // IMPORTANT: Must get values from the $form_states since sub-elements
@@ -811,18 +876,27 @@ public static function validateWebformMultiple(&$element, FormStateInterface $fo
     // @see \Drupal\webform\Element\WebformOtherBase::validateWebformOther
     $values = NestedArray::getValue($form_state->getValues(), $element['#parents']);
 
-    // Validate unique keys.
-    if ($error_message = static::validateUniqueKeys($element, $values['items'])) {
-      $form_state->setError($element, $error_message);
-      return;
-    }
+    $number_of_items_storage_key = static::getStorageKey($element, 'number_of_items');
+    $number_of_items = $form_state->get($number_of_items_storage_key);
+    if (!empty($values['items']) && ($number_of_items || $element['#cardinality'])) {
+      $items = $values['items'];
 
-    // Convert values to items and validate duplicate keys.
-    $items = static::convertValuesToItems($element, $values['items']);
+      // Validate unique keys.
+      if ($error_message = static::validateUniqueKeys($element, $items)) {
+        $form_state->setError($element, $error_message);
+        return;
+      }
 
-    // Validate required items.
-    if (!empty($element['#required']) && empty($items)) {
-      WebformElementHelper::setRequiredError($element, $form_state);
+      // Convert values to items and validate duplicate keys.
+      $items = static::convertValuesToItems($element, $items);
+
+      // Validate required items.
+      if (!empty($element['#required']) && empty($items)) {
+        WebformElementHelper::setRequiredError($element, $form_state);
+      }
+    }
+    else {
+      $items = [];
     }
 
     $element['#value'] = $items;
@@ -871,20 +945,7 @@ public static function convertValuesToItems(array $element, array $values = [])
     // Now build the associative array of items.
     $items = [];
     foreach ($values as $value) {
-      $item = NULL;
-      if (isset($value['_item_'])) {
-        $item = $value['_item_'];
-      }
-      else {
-        // Get hidden (#access: FALSE) elements in the '_handle_' column and
-        // add them to the $value.
-        // @see \Drupal\webform\Element\WebformMultiple::buildElementRow
-        if (isset($value['_hidden_']) && is_array($value['_hidden_'])) {
-          $value += $value['_hidden_'];
-        }
-        unset($value['weight'], $value['_operations_'], $value['_hidden_']);
-        $item = $value;
-      }
+      $item = static::convertValueToItem($value);
 
       // Never add an empty item.
       if (static::isEmpty($item)) {
@@ -906,6 +967,31 @@ public static function convertValuesToItems(array $element, array $values = [])
     return $items;
   }
 
+  /**
+   * Convert value array containing (elements or _item_ and weight) to an item.
+   *
+   * @param array $value
+   *   The multiple value array.
+   *
+   * @return array
+   *   An item array.
+   */
+  public static function convertValueToItem(array $value) {
+    if (isset($value['_item_'])) {
+      return $value['_item_'];
+    }
+    else {
+      // Get hidden (#access: FALSE) elements in the '_handle_' column and
+      // add them to the $value.
+      // @see \Drupal\webform\Element\WebformMultiple::buildElementRow
+      if (isset($value['_hidden_']) && is_array($value['_hidden_'])) {
+        $value += $value['_hidden_'];
+      }
+      unset($value['weight'], $value['_operations_'], $value['_hidden_']);
+      return $value;
+    }
+  }
+
   /**
    * Validate composite element has unique keys.
    *
@@ -926,15 +1012,20 @@ protected static function validateUniqueKeys(array $element, array $values) {
 
     $unique_keys = [];
     foreach ($values as $value) {
-      $item = (isset($value['_item_'])) ? $value['_item_'] : $value;
+      $item = static::convertValueToItem($value);
+
       $key_name = $element['#key'];
       $key_value = $item[$key_name];
-      if (empty($key_value)) {
+
+      // Skip empty key and item.
+      unset($item[$key_name]);
+      if (empty($key_value) && static::isEmpty($item)) {
         continue;
       }
 
       if (isset($unique_keys[$key_value])) {
-        $key_title = isset($element['#element'][$key_name]['#title']) ? $element['#element'][$key_name]['#title'] : $key_name;
+        $elements = WebformElementHelper::getFlattened($element['#element']);
+        $key_title = isset($elements[$key_name]['#title']) ? $elements[$key_name]['#title'] : $key_name;
         $t_args = ['@key' => $key_value, '%title' => $key_title];
         return t("The %title '@key' is already in use. It must be unique.", $t_args);
       }
diff --git a/web/modules/webform/src/Element/WebformOptions.php b/web/modules/webform/src/Element/WebformOptions.php
index f6d1c57545a0fcb74520eb6f965d02b9cf383f50..1d003a0cfedddd4041a7fb8de160b80842082aec 100644
--- a/web/modules/webform/src/Element/WebformOptions.php
+++ b/web/modules/webform/src/Element/WebformOptions.php
@@ -9,6 +9,7 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\Utility\WebformOptionsHelper;
+use Drupal\webform\Utility\WebformYaml;
 
 /**
  * Provides a webform element to assist in creation of options.
@@ -30,8 +31,9 @@ public function getInfo() {
       '#yaml' => FALSE,
       '#label' => t('option'),
       '#labels' => t('options'),
-      '#empty_items' => 5,
-      '#add_more' => 1,
+      '#min_items' => 3,
+      '#empty_items' => 1,
+      '#add_more_items' => 1,
       '#options_value_maxlength' => 255,
       '#options_text_maxlength' => 255,
       '#options_description' => FALSE,
@@ -85,8 +87,8 @@ public static function processWebformOptions(&$element, FormStateInterface $form
       $element['options'] = [
         '#type' => 'webform_codemirror',
         '#mode' => 'yaml',
-        '#default_value' => trim(Yaml::encode($element['#default_value'])),
-        '#placeholder' => t('Enter custom options'),
+        '#default_value' => WebformYaml::encode($element['#default_value']),
+        '#placeholder' => t('Enter custom options…'),
         '#description' => t('Key-value pairs MUST be specified as "safe_key: \'Some readable options\'". Use of only alphanumeric characters and underscores is recommended in keys. One option per line.') . '<br /><br />' .
           t('Option groups can be created by using just the group name followed by indented group options.'),
       ];
@@ -94,62 +96,84 @@ public static function processWebformOptions(&$element, FormStateInterface $form
     }
     else {
       $t_args = ['@label' => isset($element['#label']) ? Unicode::ucfirst($element['#label']) : t('Options')];
-      $properties = ['#label', '#labels', '#empty_items', '#add_more'];
+      $properties = ['#label', '#labels', '#min_items', '#empty_items', '#add_more_items'];
 
       $element['options'] = array_intersect_key($element, array_combine($properties, $properties)) + [
         '#type' => 'webform_multiple',
         '#header' => TRUE,
+        '#key' => 'value',
         '#default_value' => (isset($element['#default_value'])) ? static::convertOptionsToValues($element['#default_value'], $element['#options_description']) : [],
+        '#add_more_input_label' => t('more options'),
       ];
 
       if ($element['#options_description']) {
         $element['options']['#element'] = [
-          'value' => [
-            '#type' => 'textfield',
+          'option_value' => [
+            '#type' => 'container',
             '#title' => t('@label value', $t_args),
-            '#title_display' => t('invisible'),
-            '#placeholder' => t('Enter value'),
-            '#attributes' => ['class' => ['js-webform-options-value']],
-            '#maxlength' => $element['#options_value_maxlength'],
+            '#help' => t('A unique value stored in the database.'),
+            'value' => [
+              '#type' => 'textfield',
+              '#title' => t('@label value', $t_args),
+              '#title_display' => 'invisible',
+              '#placeholder' => t('Enter value…'),
+              '#attributes' => ['class' => ['js-webform-options-sync']],
+              '#maxlength' => $element['#options_value_maxlength'],
+              '#error_no_message' => TRUE,
+            ],
           ],
-          'option' => [
+          'option_text' => [
             '#type' => 'container',
-            '#title' => t('@label text/description', $t_args),
-            '#title_display' => t('invisible'),
+            '#title' => t('@label text / description', $t_args),
+            '#help' => t('Enter text and description to be displayed on the form.'),
             'text' => [
               '#type' => 'textfield',
               '#title' => t('@label text', $t_args),
-              '#title_display' => t('invisible'),
-              '#placeholder' => t('Enter text'),
+              '#title_display' => 'invisible',
+              '#placeholder' => t('Enter text…'),
               '#maxlength' => $element['#options_text_maxlength'],
+              '#error_no_message' => TRUE,
             ],
             'description' => [
               '#type' => 'textarea',
               '#title' => t('@label description', $t_args),
-              '#title_display' => t('invisible'),
-              '#placeholder' => t('Enter description'),
+              '#title_display' => 'invisible',
+              '#placeholder' => t('Enter description…'),
               '#rows' => 2,
               '#maxlength' => $element['#options_description_maxlength'],
+              '#error_no_message' => TRUE,
             ],
           ],
         ];
       }
       else {
         $element['options']['#element'] = [
-          'value' => [
-            '#type' => 'textfield',
+          'option_value' => [
+            '#type' => 'container',
             '#title' => t('@label value', $t_args),
-            '#title_display' => t('invisible'),
-            '#placeholder' => t('Enter value'),
-            '#attributes' => ['class' => ['js-webform-options-value']],
-            '#maxlength' => $element['#options_value_maxlength'],
+            '#help' => t('A unique value stored in the database.'),
+            'value' => [
+              '#type' => 'textfield',
+              '#title' => t('@label value', $t_args),
+              '#title_display' => 'invisible',
+              '#placeholder' => t('Enter value…'),
+              '#attributes' => ['class' => ['js-webform-options-sync']],
+              '#maxlength' => $element['#options_value_maxlength'],
+              '#error_no_message' => TRUE,
+            ],
           ],
-          'text' => [
-            '#type' => 'textfield',
+          'option_text' => [
+            '#type' => 'container',
             '#title' => t('@label text', $t_args),
-            '#title_display' => t('invisible'),
-            '#placeholder' => t('Enter text'),
-            '#maxlength' => $element['#options_text_maxlength'],
+            '#help' => t('Text to be displayed on the form.'),
+            'text' => [
+              '#type' => 'textfield',
+              '#title' => t('@label text', $t_args),
+              '#title_display' => 'invisible',
+              '#placeholder' => t('Enter text…'),
+              '#maxlength' => $element['#options_text_maxlength'],
+              '#error_no_message' => TRUE,
+            ],
           ],
         ];
       }
@@ -163,6 +187,10 @@ public static function processWebformOptions(&$element, FormStateInterface $form
    * Validates webform options element.
    */
   public static function validateWebformOptions(&$element, FormStateInterface $form_state, &$complete_form) {
+    if ($form_state->hasAnyErrors()) {
+      return;
+    }
+
     $options_value = NestedArray::getValue($form_state->getValues(), $element['options']['#parents']);
 
     if (is_string($options_value)) {
@@ -200,11 +228,10 @@ public static function validateWebformOptions(&$element, FormStateInterface $for
   public static function convertValuesToOptions(array $values = NULL, $options_description = FALSE) {
     $options = [];
     if ($values && is_array($values)) {
-      foreach ($values as $value) {
-        $option_value = $value['value'];
-        $option_text = $value['text'];
-        if ($options_description && !empty($value['description'])) {
-          $option_text .= WebformOptionsHelper::DESCRIPTION_DELIMITER . $value['description'];
+      foreach ($values as $option_value => $option) {
+        $option_text = $option['text'];
+        if ($options_description && !empty($option['description'])) {
+          $option_text .= WebformOptionsHelper::DESCRIPTION_DELIMITER . $option['description'];
         }
 
         // Populate empty option value or option text.
@@ -222,7 +249,7 @@ public static function convertValuesToOptions(array $values = NULL, $options_des
   }
 
   /**
-   * Convert options to values for yamform_multiple element.
+   * Convert options to values for webform_multiple element.
    *
    * @param array $options
    *   An array of options.
@@ -237,10 +264,10 @@ public static function convertOptionsToValues(array $options = [], $options_desc
     foreach ($options as $value => $text) {
       if ($options_description && strpos($text, WebformOptionsHelper::DESCRIPTION_DELIMITER) !== FALSE) {
         list($text, $description) = explode(WebformOptionsHelper::DESCRIPTION_DELIMITER, $text);
-        $values[] = ['value' => $value, 'text' => $text, 'description' => $description];
+        $values[$value] = ['text' => $text, 'description' => $description];
       }
       else {
-        $values[] = ['value' => $value, 'text' => $text];
+        $values[$value] = ['text' => $text];
       }
     }
     return $values;
diff --git a/web/modules/webform/src/Element/WebformOtherBase.php b/web/modules/webform/src/Element/WebformOtherBase.php
index 4f4d5dfcf6d3605b5ac87009984e90a1444e5ea2..625c0cf9e14c9a44296dc265be0c450bb8e4e328 100644
--- a/web/modules/webform/src/Element/WebformOtherBase.php
+++ b/web/modules/webform/src/Element/WebformOtherBase.php
@@ -5,7 +5,6 @@
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Form\OptGroup;
-use Drupal\Core\Render\Element\CompositeFormElementTrait;
 use Drupal\Core\Render\Element\FormElement;
 use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\Utility\WebformOptionsHelper;
@@ -15,7 +14,7 @@
  */
 abstract class WebformOtherBase extends FormElement {
 
-  use CompositeFormElementTrait;
+  use WebformCompositeFormElementTrait;
 
   /**
    * Other option value.
@@ -37,8 +36,11 @@ abstract class WebformOtherBase extends FormElement {
   protected static $properties = [
     '#title',
     '#required',
+    '#required_error',
     '#options',
     '#options_display',
+    '#options_randomize',
+    '#options_description_display',
     '#default_value',
     '#attributes',
   ];
@@ -55,7 +57,7 @@ public function getInfo() {
         [$class, 'processAjaxForm'],
       ],
       '#pre_render' => [
-        [$class, 'preRenderCompositeFormElement'],
+        [$class, 'preRenderWebformCompositeFormElement'],
       ],
       '#options' => [],
       '#other__option_delimiter' => ', ',
@@ -101,21 +103,21 @@ public static function processWebformOther(&$element, FormStateInterface $form_s
     $element['#tree'] = TRUE;
 
     $element[$type]['#type'] = static::$type;
+    $element[$type]['#webform_element'] = TRUE;
     $element[$type] += array_intersect_key($element, array_combine($properties, $properties));
     $element[$type]['#title_display'] = 'invisible';
     if (!isset($element[$type]['#options'][static::OTHER_OPTION])) {
-      $element[$type]['#options'][static::OTHER_OPTION] = (!empty($element['#other__option_label'])) ? $element['#other__option_label'] : t('Other...');
+      $element[$type]['#options'][static::OTHER_OPTION] = (!empty($element['#other__option_label'])) ? $element['#other__option_label'] : t('Other…');
     }
     $element[$type]['#error_no_message'] = TRUE;
 
-    // Disable label[for] which does not point to any specific element.
-    // @see webform_preprocess_form_element_label()
-    if (in_array($type, ['radios', 'checkboxes', 'buttons'])) {
-      $element['#label_attributes']['for'] = FALSE;
-    }
+    // Prevent nested fieldset by removing fieldset theme wrapper around
+    // radios and checkboxes.
+    // @see \Drupal\Core\Render\Element\CompositeFormElementTrait
+    $element[$type]['#pre_render'] = [];
 
     // Build other textfield.
-    $element['other']['#error_no_message'] = TRUE;
+    $element += ['other' => []];
     foreach ($element as $key => $value) {
       if (strpos($key, '#other__') === 0) {
         $other_key = str_replace('#other__', '#', $key);
@@ -126,7 +128,8 @@ public static function processWebformOther(&$element, FormStateInterface $form_s
     }
     $element['other'] += [
       '#type' => 'textfield',
-      '#placeholder' => t('Enter other...'),
+      '#webform_element' => TRUE,
+      '#placeholder' => t('Enter other…'),
     ];
     if (!isset($element['other']['#title'])) {
       $element['other'] += [
@@ -150,9 +153,21 @@ public static function processWebformOther(&$element, FormStateInterface $form_s
       $element['other']['#parents'] = array_merge($element['#parents'], ['other']);
     }
 
-    // Add js trigger to fieldset.
-    $element['#attributes']['class'][] = "js-webform-$type-other";
-    $element['#attributes']['class'][] = "webform-$type-other";
+    // Initialize the other element to allow for webform enhancements.
+    /** @var \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager */
+    $element_manager = \Drupal::service('plugin.manager.webform.element');
+    $element_manager->buildElement($element['other'], $complete_form, $form_state);
+
+    // Add js trigger attributes to the composite wrapper.
+    // @see \Drupal\webform\Element\WebformCompositeFormElementTrait
+    $is_form_element_wrapper = (isset($element['#wrapper_type']) && $element['#wrapper_type'] === 'form_element');
+    $wrapper_attributes = ($is_form_element_wrapper) ? '#wrapper_attributes' : '#attributes';
+    $element[$wrapper_attributes]['class'][] = "js-webform-$type-other";
+    $element[$wrapper_attributes]['class'][] = "webform-$type-other";
+
+    // Apply the element id to the wrapper so that inline form errors point
+    // to the correct element.
+    $element['#attributes']['id'] = $element['#id'];
 
     // Remove options.
     unset($element['#options']);
@@ -174,58 +189,47 @@ public static function processWebformOther(&$element, FormStateInterface $form_s
    * Validates an other element.
    */
   public static function validateWebformOther(&$element, FormStateInterface $form_state, &$complete_form) {
+    $type = static::getElementType();
+
     // Determine if the element is visible. (#access !== FALSE)
     $has_access = (!isset($element['#access']) || $element['#access'] === TRUE);
 
-    // Remove 'webform_' prefix from type.
-    $type = str_replace('webform_', '', static::$type);
+    // Determine if the element has mulitple values.
+    $is_multiple = static::isMultiple($element);
 
     // Get value.
     $value = NestedArray::getValue($form_state->getValues(), $element['#parents']);
 
     // Get return value.
-    $return_value = [];
-    $element_value = $value[$type];
-    $other_value = $value['other'];
-    $required_error_title = (isset($element['#title'])) ? $element['#title'] : NULL;
-    if (static::isMultiple($element)) {
-      $element_value = array_filter($element_value);
-      $element_value = array_combine($element_value, $element_value);
-      $return_value += $element_value;
-      if (isset($return_value[static::OTHER_OPTION])) {
-        unset($return_value[static::OTHER_OPTION]);
-        if ($has_access && $other_value === '') {
-          WebformElementHelper::setRequiredError($element, $form_state, $required_error_title);
-        }
-        else {
-          $return_value += [$other_value => $other_value];
-        }
-      }
-    }
-    else {
-      $return_value = $element_value;
-      if ($element_value == static::OTHER_OPTION) {
-        if ($has_access && $other_value === '') {
-          WebformElementHelper::setRequiredError($element, $form_state, $required_error_title);
-          $return_value = '';
-        }
-        else {
-          $return_value = $other_value;
-        }
-      }
-    }
+    $return_value = static::processValue($element, $value);
 
     // Determine if the return value is empty.
-    if (static::isMultiple($element)) {
+    if ($is_multiple) {
       $is_empty = (empty($return_value)) ? TRUE : FALSE;
     }
     else {
       $is_empty = ($return_value === '' || $return_value === NULL) ? TRUE : FALSE;
     }
 
-    // Handler required validation.
-    if ($element['#required'] && $is_empty && $has_access) {
-      WebformElementHelper::setRequiredError($element, $form_state, $required_error_title);
+    // Determine if there is an other value and is the other value empty.
+    $element_value = (array) $value[$type];
+    $other_value = $value['other'];
+    if ($element_value) {
+      $element_value = array_filter($element_value);
+      $element_value = array_combine($element_value, $element_value);
+    }
+    $other_is_empty = (isset($element_value[static::OTHER_OPTION]) && $other_value === '');
+
+    // Display missing other or missing value error.
+    if ($has_access) {
+      $required_error_title = (isset($element['#title'])) ? $element['#title'] : NULL;
+      if ($other_is_empty) {
+        WebformElementHelper::setRequiredError($element['other'], $form_state, $required_error_title);
+      }
+      elseif ($element['#required'] && $is_empty) {
+        WebformElementHelper::setRequiredError($element, $form_state, $required_error_title);
+        $element['other']['#error_no_message'] = TRUE;
+      }
     }
 
     $form_state->setValueForElement($element[$type], NULL);
@@ -235,10 +239,54 @@ public static function validateWebformOther(&$element, FormStateInterface $form_
     $form_state->setValueForElement($element, $return_value);
   }
 
+  /**
+   * Processed other element's submitted value.
+   *
+   * @param array $element
+   *   The element.
+   * @param array $value
+   *   The submitted value.
+   *
+   * @return array|string
+   *   An array of values or a string.
+   */
+  public static function processValue(array $element, array $value) {
+    $type = static::getElementType();
+
+    $element_value = $value[$type];
+    $other_value = $value['other'];
+
+    if (static::isMultiple($element)) {
+      $return_value = array_filter($element_value);
+      $return_value = array_combine($return_value, $return_value);
+      if (isset($return_value[static::OTHER_OPTION])) {
+        unset($return_value[static::OTHER_OPTION]);
+        if ($other_value !== '') {
+          $return_value += [$other_value => $other_value];
+        }
+      }
+      return $return_value;
+    }
+    else {
+      return ($element_value === static::OTHER_OPTION) ? $other_value : $element_value;
+    }
+  }
+
   /****************************************************************************/
   // Helper functions.
   /****************************************************************************/
 
+  /**
+   * Get the element type.
+   *
+   * @return string
+   *   The element type.
+   */
+  public static function getElementType() {
+    // Remove 'webform_' prefix from type.
+    return str_replace('webform_', '', static::$type);
+  }
+
   /**
    * Determine if the webform element contains multiple values.
    *
diff --git a/web/modules/webform/src/Element/WebformPermissions.php b/web/modules/webform/src/Element/WebformPermissions.php
index b7b420befbe12103438e0fa8c615432770b4214e..43a0519c7c7eb30036e3066ca1ecffa50ff60e23 100644
--- a/web/modules/webform/src/Element/WebformPermissions.php
+++ b/web/modules/webform/src/Element/WebformPermissions.php
@@ -43,14 +43,15 @@ public static function processSelect(&$element, FormStateInterface $form_state,
       $options[$display_name][$perm] = strip_tags($perm_item['title']);
     }
     $element['#options'] = $options;
-
-    WebformElementHelper::enhanceSelect($element, TRUE);
+    $element['#select2'] = TRUE;
 
     // Must convert this element['#type'] to a 'select' to prevent
     // "Illegal choice %choice in %name element" validation error.
     // @see \Drupal\Core\Form\FormValidator::performRequiredValidation
     $element['#type'] = 'select';
 
+    WebformElementHelper::process($element);
+
     return parent::processSelect($element, $form_state, $complete_form);
   }
 
diff --git a/web/modules/webform/src/Element/WebformRating.php b/web/modules/webform/src/Element/WebformRating.php
index 5b18b7f00d1889db08fec52f9f61e128a53e55bd..67a736adf3964a54cf273ee04036b2366d3608b9 100644
--- a/web/modules/webform/src/Element/WebformRating.php
+++ b/web/modules/webform/src/Element/WebformRating.php
@@ -2,8 +2,10 @@
 
 namespace Drupal\webform\Element;
 
+use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element\Range;
 use Drupal\Core\Render\Element;
+use Drupal\webform\Utility\WebformElementHelper;
 
 /**
  * Provides a webform element for entering a rating.
@@ -23,6 +25,9 @@ public function getInfo() {
       '#step' => 1,
       '#star_size' => 'medium',
       '#reset' => FALSE,
+      '#process' => [
+        [$class, 'processWebformRating'],
+      ],
       '#pre_render' => [
         [$class, 'preRenderWebformRating'],
       ],
@@ -30,6 +35,15 @@ public function getInfo() {
     ] + parent::getInfo();
   }
 
+  /**
+   * Expand rating elements.
+   */
+  public static function processWebformRating(&$element, FormStateInterface $form_state, &$complete_form) {
+    // Add validate callback.
+    $element['#element_validate'] = [[get_called_class(), 'validateWebformRating']];
+    return $element;
+  }
+
   /**
    * Prepares a #type 'webform_rating' render element for input.html.twig.
    *
@@ -62,8 +76,8 @@ public static function preRenderWebformRating(array $element) {
    * @param array $element
    *   A rating element.
    *
-   * @return string
-   *   The RateIt div tag.
+   * @return array
+   *   A renderable array containing the RateIt div tag.
    *
    * @see https://github.com/gjunge/rateit.js/wiki
    */
@@ -89,9 +103,9 @@ public static function buildRateIt(array $element) {
       'data-rateit-readonly' => $is_readonly ? 'true' : 'false',
     ];
 
-    // Set range element's #id.
-    if (isset($element['#id'])) {
-      $attributes['data-rateit-backingfld'] = '#' . $element['#id'];
+    // Set range element's selector based on its parents.
+    if (isset($element['#attributes']['data-drupal-selector'])) {
+      $attributes['data-rateit-backingfld'] = '[data-drupal-selector="' . $element['#attributes']['data-drupal-selector'] . '"]';
     }
 
     // Set value for HTML preview.
@@ -120,7 +134,17 @@ public static function buildRateIt(array $element) {
         'library' => ['webform/webform.element.rating'],
       ],
     ];
+  }
 
+  /**
+   * Validates a rating element.
+   */
+  public static function validateWebformRating(&$element, FormStateInterface $form_state, &$complete_form) {
+    $value = $element['#value'];
+    $has_access = (!isset($element['#access']) || $element['#access'] === TRUE);
+    if ($has_access && !empty($element['#required']) && ($value === '0' || $value === '')) {
+      WebformElementHelper::setRequiredError($element, $form_state);
+    }
   }
 
 }
diff --git a/web/modules/webform/src/Element/WebformSelectOther.php b/web/modules/webform/src/Element/WebformSelectOther.php
index 8f2f1091bb2197add22432858e6a4958a5cfc94a..ea49ec35a7a53887d3a4f74de3697893d83b163d 100644
--- a/web/modules/webform/src/Element/WebformSelectOther.php
+++ b/web/modules/webform/src/Element/WebformSelectOther.php
@@ -21,6 +21,7 @@ class WebformSelectOther extends WebformOtherBase {
    * {@inheritdoc}
    */
   protected static $properties = [
+    '#title',
     '#required',
     '#options',
     '#default_value',
diff --git a/web/modules/webform/src/Element/WebformSignature.php b/web/modules/webform/src/Element/WebformSignature.php
index 1dad714e6bf26c957b00d7ed0ab70dbfc9d3db61..3b26d6a9da4648748df282528104bcd67feaaff4 100644
--- a/web/modules/webform/src/Element/WebformSignature.php
+++ b/web/modules/webform/src/Element/WebformSignature.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\webform\Element;
 
+use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element\FormElement;
 use Drupal\Core\Render\Element;
 
@@ -20,6 +21,7 @@ public function getInfo() {
     return [
       '#input' => TRUE,
       '#process' => [
+        [$class, 'processWebformSignature'],
         [$class, 'processAjaxForm'],
         [$class, 'processGroup'],
       ],
@@ -34,6 +36,15 @@ public function getInfo() {
     ];
   }
 
+  /**
+   * Processes a signature webform element.
+   */
+  public static function processWebformSignature(&$element, FormStateInterface $form_state, &$complete_form) {
+    // Remove 'for' from the element's label.
+    $element['#label_attributes']['webform-remove-for-attribute'] = TRUE;
+    return $element;
+  }
+
   /**
    * Prepares a #type 'webform_signature' render element for input.html.twig.
    *
diff --git a/web/modules/webform/src/Element/WebformSubmissionInformation.php b/web/modules/webform/src/Element/WebformSubmissionInformation.php
new file mode 100644
index 0000000000000000000000000000000000000000..a7f2b6db0af3dece3d38d8cd7679bbeeb242f643
--- /dev/null
+++ b/web/modules/webform/src/Element/WebformSubmissionInformation.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\webform\Element;
+
+use Drupal\Core\Render\Element\RenderElement;
+
+/**
+ * Provides a render element to display webform submission information.
+ *
+ * @RenderElement("webform_submission_information")
+ */
+class WebformSubmissionInformation extends RenderElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+
+    return [
+      '#theme' => 'webform_submission_information',
+      '#webform_submission' => NULL,
+      '#source_entity' => NULL,
+      '#pre_render' => [
+        [$class, 'preRenderWebformSubmissionInformation'],
+      ],
+      '#theme_wrappers' => ['details'],
+      '#summary_attributes' => [],
+    ];
+  }
+
+  /**
+   * Create webform submission information for rendering.
+   *
+   * @param array $element
+   *   An associative array containing the properties and children of the
+   *   element.
+   *
+   * @return array
+   *   The modified element with webform submission information.
+   */
+  public static function preRenderWebformSubmissionInformation(array $element) {
+    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
+    $webform_submission = $element['#webform_submission'];
+    $webform = $webform_submission->getWebform();
+
+    // Add title.
+    $element += [
+      '#title' => t('Submission information'),
+    ];
+
+    // Add details attributes.
+    $element['#attributes']['data-webform-element-id'] = $webform->id() . '-submission-information';
+    $element['#attributes']['class'] = ['webform-submission-information'];
+
+    return $element;
+  }
+
+}
diff --git a/web/modules/webform/src/Element/WebformSubmissionNavigation.php b/web/modules/webform/src/Element/WebformSubmissionNavigation.php
new file mode 100644
index 0000000000000000000000000000000000000000..cdfe4c936cb9f0576963675f3a99135c057aa943
--- /dev/null
+++ b/web/modules/webform/src/Element/WebformSubmissionNavigation.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\webform\Element;
+
+use Drupal\Core\Render\Element\RenderElement;
+
+/**
+ * Provides a render element to display webform submission navigation.
+ *
+ * @RenderElement("webform_submission_navigation")
+ */
+class WebformSubmissionNavigation extends RenderElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    return [
+      '#theme' => 'webform_submission_navigation',
+      '#webform_submission' => NULL,
+    ];
+  }
+
+}
diff --git a/web/modules/webform/src/Element/WebformSubmissionViews.php b/web/modules/webform/src/Element/WebformSubmissionViews.php
new file mode 100644
index 0000000000000000000000000000000000000000..ad3f423567f062d56275903d3293b7655fe11dfc
--- /dev/null
+++ b/web/modules/webform/src/Element/WebformSubmissionViews.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace Drupal\webform\Element;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\views\Entity\View;
+
+/**
+ * Provides a form element for selecting webform submission views.
+ *
+ * @FormElement("webform_submission_views")
+ */
+class WebformSubmissionViews extends WebformMultiple {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function processWebformMultiple(&$element, FormStateInterface $form_state, &$complete_form) {
+    if (!\Drupal::moduleHandler()->moduleExists('views')) {
+      $element['#element_validate'] = [[get_called_class(), 'emptyValue']];
+      return $element;
+    }
+
+    $element['#key'] = 'name';
+    $element['#header'] = TRUE;
+    $element['#empty_items'] = 0;
+    $element['#min_items'] = 1;
+    $element['#add_more_input_label'] = t('more submission views');
+
+    // Build element.
+    $element['#element'] = [];
+
+    // Name / Title / View.
+    $view_options = [];
+    /** @var \Drupal\views\ViewEntityInterface[] $views */
+    $views = View::loadMultiple();
+    foreach ($views as $view) {
+      // Only include webform submission views.
+      if ($view->get('base_table') !== 'webform_submission' || $view->get('base_field') !== 'sid') {
+        continue;
+      }
+
+      $optgroup = $view->label();
+      $displays = $view->get('display');
+      foreach ($displays as $display_id => $display) {
+        // Only include embed displays.
+        if ($display['display_plugin'] === 'embed') {
+          $view_options[$optgroup][$view->id() . ':' . $display_id] = $optgroup . ': ' . $display['display_title'];
+        }
+      }
+    }
+    $element['#element']['name_title_view'] = [
+      '#type' => 'container',
+      '#title' => t('View / Name / Title'),
+      '#help' => '<b>' . t('View') . ':</b> ' . t('A webform submission embed display. The selected view should also include contextual filters. {webform_id}/{source_entity_type}/{source_entity_id}/{account_id}/{in_draft}') .
+        '<hr/>' . '<b>' . t('Name') . ':</b> ' . t('The name to be displayed in the URL when there are multiple submission views available.') .
+        '<hr/>' . '<b>' . t('Options') . ':</b> ' . t('The title to be display in the dropdown menu when there are multiple submission views available.'),
+      'view' => [
+        '#type' => 'select',
+        '#title' => t('View'),
+        '#title_display' => 'invisible',
+        '#empty_option' => t('Select view…'),
+        '#options' => $view_options,
+        '#error_no_message' => TRUE,
+      ],
+      'name' => [
+        '#type' => 'textfield',
+        '#title' => t('Name'),
+        '#title_display' => 'invisible',
+        '#placeholder' => t('Enter name…'),
+        '#size' => 20,
+        '#pattern' => '^[-_a-z0-9]+$',
+        '#error_no_message' => TRUE,
+      ],
+      'title' => [
+        '#type' => 'textfield',
+        '#title' => t('Title'),
+        '#title_display' => 'invisible',
+        '#placeholder' => t('Enter title…'),
+        '#size' => 20,
+        '#error_no_message' => TRUE,
+      ],
+    ];
+
+    // Global routes.
+    if (!empty($element['#global'])) {
+      $global_route_options = [
+        'entity.webform_submission.collection' => t('Submissions'),
+        'entity.webform_submission.user' => t('User'),
+      ];
+      $element['#element']['global_routes'] = [
+        '#type' => 'checkboxes',
+        '#title' => t('Apply to global'),
+          '#help' => t('Display the selected view on the below paths') .
+            '<hr/><b>' . t('Submissions') . ':</b><br/>/admin/structure/webform/submissions/manage' .
+            '<hr/><b>' . t('User') . ':</b><br/>/user/{user}/submissions',
+        '#options' => $global_route_options,
+        '#element_validate' => [['\Drupal\webform\Utility\WebformElementHelper', 'filterValues']],
+        '#error_no_message' => TRUE,
+      ];
+    }
+
+    // Webform routes.
+    $webform_route_options = [
+      'entity.webform.results_submissions' => t('Submissions'),
+      'entity.webform.user.drafts' => t('User drafts'),
+      'entity.webform.user.submissions' => t('User submissions'),
+    ];
+    $element['#element']['webform_routes'] = [
+      '#type' => 'checkboxes',
+      '#title' => t('Apply to webform'),
+        '#help' => t('Display the selected view on the below paths') .
+          '<hr/><b>' . t('Submissions') . ':</b><br/>/admin/structure/webform/manage/{webform}/results/submissions' .
+          '<hr/><b>' . t('User drafts') . ':</b><br/>/webform/{webform}/drafts' .
+          '<hr/><b>' . t('User submissions') . ':</b><br/>/webform/{webform}/submissions',
+      '#options' => $webform_route_options,
+      '#element_validate' => [['\Drupal\webform\Utility\WebformElementHelper', 'filterValues']],
+      '#error_no_message' => TRUE,
+    ];
+
+    // Node routes.
+    if (\Drupal::moduleHandler()->moduleExists('webform_node')) {
+      $node_route_options = [
+        'entity.node.webform.results_submissions' => t('Submissions'),
+        'entity.node.webform.user.drafts' => t('User drafts'),
+        'entity.node.webform.user.submissions' => t('User submissions'),
+      ];
+      $element['#element']['node_routes'] = [
+        '#type' => 'checkboxes',
+        '#title' => t('Apply to node'),
+        '#help' =>
+          t('Display the selected view on the below paths') .
+          '<hr/><b>' . t('Submissions') . ':</b><br/>/node/{node}/webform/results/submissions' .
+          '<hr/>' . '<b>' . t('User drafts') . ':</b><br/>/node/{node}/webform/drafts' .
+          '<hr/>' . '<b>' . t('User submissions') . ':</b><br/>/node/{node}/webform/submissions',
+        '#options' => $node_route_options,
+        '#element_validate' => [['\Drupal\webform\Utility\WebformElementHelper', 'filterValues']],
+        '#error_no_message' => TRUE,
+      ];
+    }
+
+    parent::processWebformMultiple($element, $form_state, $complete_form);
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function validateWebformMultiple(&$element, FormStateInterface $form_state, &$complete_form) {
+    if (!\Drupal::moduleHandler()->moduleExists('views')) {
+      $element['#value'] = [];
+      $form_state->setValueForElement($element, []);
+      return;
+    }
+
+    parent::validateWebformMultiple($element, $form_state, $complete_form);
+    $items = NestedArray::getValue($form_state->getValues(), $element['#parents']);
+    foreach ($items as $name => &$item) {
+      // Remove empty view references.
+      if ($name === '' && empty($item['view']) && empty($item['global_routes']) && empty($item['webform_routes']) && empty($item['node_routes'])) {
+        unset($items[$name]);
+        continue;
+      }
+
+      if ($name === '') {
+        $form_state->setError($element, t('Name is required.'));
+      }
+      if (empty($item['title'])) {
+        $form_state->setError($element, t('Title is required.'));
+      }
+      if (empty($item['view'])) {
+        $form_state->setError($element, t('View name/display id is required.'));
+      }
+    }
+
+    $element['#value'] = $items;
+    $form_state->setValueForElement($element, $items);
+  }
+
+  /**
+   * Form validate callback which clears the submitted value.
+   */
+  public static function emptyValue(&$element, FormStateInterface $form_state, &$complete_form) {
+    $element['#value'] = [];
+    $form_state->setValueForElement($element, []);
+  }
+
+}
diff --git a/web/modules/webform/src/Element/WebformSubmissionViewsReplace.php b/web/modules/webform/src/Element/WebformSubmissionViewsReplace.php
new file mode 100644
index 0000000000000000000000000000000000000000..cd1df1b82c4c3219e8c404b976caab3dd5ab1f56
--- /dev/null
+++ b/web/modules/webform/src/Element/WebformSubmissionViewsReplace.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Drupal\webform\Element;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element\FormElement;
+
+/**
+ * Provides a form element for selecting webform submission views replacement routes.
+ *
+ * @FormElement("webform_submission_views_replace")
+ */
+class WebformSubmissionViewsReplace extends FormElement {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return [
+      '#input' => TRUE,
+      '#process' => [
+        [$class, 'processWebformSubmissionViewsReplace'],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
+    if ($input === FALSE) {
+      if (!isset($element['#default_value'])) {
+        $element['#default_value'] = [];
+      }
+      return $element['#default_value'];
+    }
+    else {
+      return $input;
+    }
+  }
+
+  /**
+   * Processes a ng webform submission views replacement element.
+   */
+  public static function processWebformSubmissionViewsReplace(&$element, FormStateInterface $form_state, &$complete_form) {
+    $is_global = (!empty($element['#global'])) ? TRUE : FALSE;
+    $element['#tree'] = TRUE;
+
+    $element['#value'] = (!is_array($element['#value'])) ? [] : $element['#value'];
+    $element['#value'] += [
+      'global_routes' => [],
+      'webform_routes' => [],
+      'node_routes' => [],
+    ];
+
+    // Global routes.
+    if ($is_global) {
+      $element['global_routes'] = [
+        '#type' => 'checkboxes',
+        '#title' => t('Replace the global results with submission views'),
+        '#options' => [
+          'entity.webform_submission.collection' => t('Submissions'),
+          'entity.webform_submission.user' => t('User'),
+        ],
+        '#default_value' => $element['#value']['global_routes'],
+        '#element_validate' => [['\Drupal\webform\Utility\WebformElementHelper', 'filterValues']],
+      ];
+    }
+
+    // Webform routes.
+    $webform_routes_options = [
+      'entity.webform.results_submissions' => t('Submissions'),
+      'entity.webform.user.drafts' => t('User drafts'),
+      'entity.webform.user.submissions' => t('User submissions'),
+    ];
+    if (!$is_global) {
+      $default_webform_routes = \Drupal::configFactory()->get('webform.settings')->get('settings.default_submission_views_replace.webform_routes') ?: [];
+      if ($webform_routes_options) {
+        $webform_routes_options = array_diff_key($webform_routes_options, array_flip($default_webform_routes));
+      }
+    }
+    $element['webform_routes'] = [
+      '#type' => 'checkboxes',
+      '#title' => t('Replace the webform results with submission views'),
+      '#options' => $webform_routes_options,
+      '#default_value' => ($webform_routes_options) ? $element['#value']['webform_routes'] : [],
+      '#access' => ($webform_routes_options) ? TRUE : FALSE,
+      '#element_validate' => [['\Drupal\webform\Utility\WebformElementHelper', 'filterValues']],
+    ];
+
+    // Node routes.
+    $node_routes_options = [
+      'entity.node.webform.results_submissions' => t('Submissions'),
+      'entity.node.webform.user.drafts' => t('User drafts'),
+      'entity.node.webform.user.submissions' => t('User submissions'),
+    ];
+    if (!$is_global) {
+      $default_node_routes = \Drupal::configFactory()->get('webform.settings')->get('settings.default_submission_views_replace.node_routes') ?: [];
+      if ($default_node_routes) {
+        $node_routes_options = array_diff_key($node_routes_options, array_flip($default_node_routes));
+      }
+    }
+    $element['node_routes'] = [
+      '#type' => 'checkboxes',
+      '#title' => t('Replace the node results with submission views'),
+      '#options' => $node_routes_options,
+      '#default_value' => ($node_routes_options) ? $element['#value']['node_routes'] : [],
+      '#access' => ($node_routes_options && \Drupal::moduleHandler()->moduleExists('webform_node')) ? TRUE : FALSE,
+      '#element_validate' => [['\Drupal\webform\Utility\WebformElementHelper', 'filterValues']],
+    ];
+
+    return $element;
+  }
+
+}
diff --git a/web/modules/webform/src/Element/WebformTableSelectSort.php b/web/modules/webform/src/Element/WebformTableSelectSort.php
index 7365be67d26b4db82fef7e1fa1976e4fe8b26823..7c6ed9920c6d26fc9df43e47dafde720787914e1 100644
--- a/web/modules/webform/src/Element/WebformTableSelectSort.php
+++ b/web/modules/webform/src/Element/WebformTableSelectSort.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\Element\Table;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\webform\Plugin\WebformElement\TableSelect;
 
 /**
  * Provides a webform element for a sortable tableselect element.
@@ -140,6 +141,11 @@ public static function preRenderWebformTableSelectSort($element) {
     $element['#header'] = $header;
     $element['#rows'] = $rows;
 
+    // Attach table select UX improvements.
+    $element['#attributes']['class'][] = 'webform-tableselect';
+    $element['#attributes']['class'][] = 'js-webform-tableselect';
+    $element['#attached']['library'][] = 'webform/webform.element.tableselect';
+
     // Attach table sort.
     $element['#attributes']['class'][] = 'js-tableselect-sort';
     $element['#attributes']['class'][] = 'tableselect-sort';
@@ -197,17 +203,13 @@ public static function processWebformTableSelectSort(&$element, FormStateInterfa
       foreach ($element['#options'] as $key => $choice) {
         // Do not overwrite manually created children.
         if (!isset($element[$key])) {
-          $checkbox_title = '';
-          $weight_title = '';
-          if (isset($element['#options'][$key]['title']) && is_array($element['#options'][$key]['title'])) {
-            if (!empty($element['#options'][$key]['title']['data']['#title'])) {
-              $checkbox_title = new TranslatableMarkup('Update @title', [
-                '@title' => $element['#options'][$key]['title']['data']['#title'],
-              ]);
-              $weight_title = new TranslatableMarkup('Weight for @title', [
-                '@title' => $element['#options'][$key]['title']['data']['#title'],
-              ]);
-            }
+          if ($title = TableSelect::getTableSelectOptionTitle($choice)) {
+            $checkbox_title = $title;
+            $weight_title = new TranslatableMarkup('Weight for @title', ['@title' => $title]);
+          }
+          else {
+            $checkbox_title = '';
+            $weight_title = '';
           }
 
           $element[$key]['checkbox'] = [
diff --git a/web/modules/webform/src/Element/WebformTableSort.php b/web/modules/webform/src/Element/WebformTableSort.php
index 88d709033bfe358fd389557d338f3eabb094d8ec..869222e255a2645154467d1804a69c86f114889d 100644
--- a/web/modules/webform/src/Element/WebformTableSort.php
+++ b/web/modules/webform/src/Element/WebformTableSort.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\Element\Table;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\webform\Plugin\WebformElement\TableSelect;
 
 /**
  * Provides a webform element for a sortable table element.
@@ -179,12 +180,8 @@ public static function processWebformTableSort(&$element, FormStateInterface $fo
         // Do not overwrite manually created children.
         if (!isset($element[$key])) {
           $weight_title = '';
-          if (isset($element['#options'][$key]['title']) && is_array($element['#options'][$key]['title'])) {
-            if (!empty($element['#options'][$key]['title']['data']['#title'])) {
-              $weight_title = new TranslatableMarkup('Weight for @title', [
-                '@title' => $element['#options'][$key]['title']['data']['#title'],
-              ]);
-            }
+          if ($title = TableSelect::getTableSelectOptionTitle($choice)) {
+            $weight_title = new TranslatableMarkup('Weight for @title', ['@title' => $title]);
           }
 
           $element[$key]['value'] = [
diff --git a/web/modules/webform/src/Element/WebformTermCheckboxes.php b/web/modules/webform/src/Element/WebformTermCheckboxes.php
index db5efbd8267a36f288454934d7ba23a38b175567..9d48f42e0c2e07b950cddc3b304ef6d2acac3f31 100644
--- a/web/modules/webform/src/Element/WebformTermCheckboxes.php
+++ b/web/modules/webform/src/Element/WebformTermCheckboxes.php
@@ -41,9 +41,7 @@ public static function processCheckboxes(&$element, FormStateInterface $form_sta
     /** @var \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository */
     $entity_repository = \Drupal::service('entity.repository');
 
-    /** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */
-    $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
-    $tree = $taxonomy_storage->loadTree($element['#vocabulary'], 0, NULL, TRUE);
+    $tree = static::loadTree($element['#vocabulary']);
 
     if (empty($element['#breadcrumb'])) {
       foreach ($tree as $item) {
@@ -71,10 +69,8 @@ protected static function getOptionsTree(array $element, $language) {
 
     /** @var \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository */
     $entity_repository = \Drupal::service('entity.repository');
-    /** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */
-    $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
 
-    $tree = $taxonomy_storage->loadTree($element['#vocabulary'], 0, NULL, TRUE);
+    $tree = static::loadTree($element['#vocabulary']);
 
     $options = [];
     foreach ($tree as $item) {
diff --git a/web/modules/webform/src/Element/WebformTermReferenceTrait.php b/web/modules/webform/src/Element/WebformTermReferenceTrait.php
index 4508ab8ab15deda1bdc8d8217c7d0fb7a9a40703..e966a31c2530302f5671cb6f858d86cef053ee83 100644
--- a/web/modules/webform/src/Element/WebformTermReferenceTrait.php
+++ b/web/modules/webform/src/Element/WebformTermReferenceTrait.php
@@ -48,10 +48,8 @@ protected static function getOptionsBreadcrumb(array $element, $language) {
 
     /** @var \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository */
     $entity_repository = \Drupal::service('entity.repository');
-    /** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */
-    $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
 
-    $tree = $taxonomy_storage->loadTree($element['#vocabulary'], 0, NULL, TRUE);
+    $tree = static::loadTree($element['#vocabulary']);
 
     $options = [];
     $breadcrumb = [];
@@ -81,10 +79,8 @@ protected static function getOptionsTree(array $element, $language) {
 
     /** @var \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository */
     $entity_repository = \Drupal::service('entity.repository');
-    /** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */
-    $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
 
-    $tree = $taxonomy_storage->loadTree($element['#vocabulary'], 0, NULL, TRUE);
+    $tree = static::loadTree($element['#vocabulary']);
 
     $options = [];
     foreach ($tree as $item) {
@@ -95,4 +91,19 @@ protected static function getOptionsTree(array $element, $language) {
     return $options;
   }
 
+  /**
+   * Finds all terms in a given vocabulary ID.
+   *
+   * @param string $vid
+   *   Vocabulary ID to retrieve terms for.
+   *
+   * @return object[]|\Drupal\taxonomy\TermInterface[]
+   *   An array of term objects that are the children of the vocabulary $vid.
+   */
+  protected static function loadTree($vid) {
+    /** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */
+    $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
+    return $taxonomy_storage->loadTree($vid, 0, NULL, TRUE);
+  }
+
 }
diff --git a/web/modules/webform/src/Element/WebformTermsOfService.php b/web/modules/webform/src/Element/WebformTermsOfService.php
index c82b377ad934eda19194dba0fa5a70b4f0190ec6..7ca66c34f4571e1069752bd7d7e485a7d3f6142d 100644
--- a/web/modules/webform/src/Element/WebformTermsOfService.php
+++ b/web/modules/webform/src/Element/WebformTermsOfService.php
@@ -43,11 +43,13 @@ public function getInfo() {
    */
   public static function preRenderCheckbox($element) {
     $element = parent::preRenderCheckbox($element);
+    $id = 'webform-terms-of-service-' . implode('_', $element['#parents']);
 
     if (empty($element['#title'])) {
       $element['#title'] = (string) t('I agree to the {terms of service}.');
     }
-    $element['#title'] = str_replace('{', '<a>', $element['#title']);
+
+    $element['#title'] = str_replace('{', '<a role="button" href="#terms">', $element['#title']);
     $element['#title'] = str_replace('}', '</a>', $element['#title']);
 
     // Change description to render array.
@@ -61,23 +63,35 @@ public static function preRenderCheckbox($element) {
     // Add terms to #description.
     $element['#description']['terms'] = [
       '#type' => 'container',
-      '#attributes' => ['class' => ['webform-terms-of-service-details', 'js-hide']],
+      '#attributes' => [
+        'id' => $id . '--description',
+        'class' => ['webform-terms-of-service-details', 'js-hide'],
+      ],
     ];
     if (!empty($element['#terms_title'])) {
       $element['#description']['terms']['title'] = [
+        '#type' => 'container',
         '#markup' => $element['#terms_title'],
-        '#prefix' => '<div class="webform-terms-of-service-details--title">',
-        '#suffix' => '</div>',
+        '#attributes' => [
+          'class' => ['webform-terms-of-service-details--title'],
+        ],
       ];
     }
     if (!empty($element['#terms_content'])) {
       $element['#description']['terms']['content'] = (is_array($element['#terms_content'])) ? $element['#terms_content'] : ['#markup' => $element['#terms_content']];
       $element['#description']['terms']['content'] += [
-        '#prefix' => '<div class="webform-terms-of-service-details--content">',
-        '#suffix' => '</div>',
+        '#type' => 'container',
+        '#attributes' => [
+          'class' => ['webform-terms-of-service-details--content'],
+        ],
       ];
     }
 
+    // Add accessibility attributes to title and content.
+    if ($element['#type'] === static::TERMS_SLIDEOUT) {
+
+    }
+
     // Set type to data attribute.
     // @see Drupal.behaviors.webformTermsOfService.
     $element['#wrapper_attributes']['data-webform-terms-of-service-type'] = $element['#terms_type'];
diff --git a/web/modules/webform/src/Element/WebformTime.php b/web/modules/webform/src/Element/WebformTime.php
index f30f73f29e4c1aa4131672304c878b9667b16af8..2785c5ba9fbfe2413f43f7cbfed48dbd5a4298ce 100644
--- a/web/modules/webform/src/Element/WebformTime.php
+++ b/web/modules/webform/src/Element/WebformTime.php
@@ -12,7 +12,7 @@
  *
  * @code
  * $form['time'] = array(
- *   '#type' => 'time',
+ *   '#type' => 'webform_time',
  *   '#title' => $this->t('Time'),
  *   '#default_value' => '12:00 AM'
  * );
@@ -35,8 +35,8 @@ public function getInfo() {
       '#theme_wrappers' => ['form_element'],
       '#timepicker' => FALSE,
       '#time_format' => 'H:i',
-      '#size' => 10,
-      '#maxlength' => 10,
+      '#size' => 12,
+      '#maxlength' => 12,
     ];
   }
 
@@ -48,9 +48,21 @@ public static function valueCallback(&$element, $input, FormStateInterface $form
       // Set default value using GNU PHP date format.
       // @see https://www.gnu.org/software/tar/manual/html_chapter/tar_7.html#Date-input-formats.
       if (!empty($element['#default_value'])) {
-        $element['#default_value'] = date('H:i', strtotime($element['#default_value']));
+        try {
+          // Evaluate if the value can be time formatted, including relative
+          // values like 'now' or '+2 hours'.
+          new \DateTime($element['#default_value']);
+        }
+        catch (\Exception $exception) {
+          \Drupal::messenger()->addError($exception->getMessage());
+          return NULL;
+        }
+        $element['#default_value'] = static::formatTime('H:i', strtotime($element['#default_value']));
         return $element['#default_value'];
       }
+      else {
+        return NULL;
+      }
     }
 
     return $input;
@@ -83,18 +95,33 @@ public static function processWebformTime(&$element, FormStateInterface $form_st
   /**
    * Webform element validation handler for #type 'webform_time'.
    *
-   * Note that #required is validated by _form_validate() already.
+   * Note that #required is validated by _form_valistatic::formatTime() already.
    */
   public static function validateWebformTime(&$element, FormStateInterface $form_state, &$complete_form) {
     $has_access = (!isset($element['#access']) || $element['#access'] === TRUE);
 
     $value = $element['#value'];
-    if ($value === '') {
+    if (trim($value) === '') {
       return;
     }
 
     $time = strtotime($value);
-    if ($time === FALSE) {
+
+    // Make the submitted value is parsable and in the specified
+    // custom format (ex: g:i A) or default format (ex: H:i).
+    //
+    // This accounts for time values generated by an HTML5 time input or
+    // the jQuery Timepicker.
+    //
+    // @see \Drupal\webform\Plugin\WebformElement\DateBase::validateDate
+    // @see https://github.com/jonthornton/jquery-timepicker
+    // @see js/webform.element.time.js
+    $is_valid_time = $time && (
+      $value == static::formatTime('H:i', $time) ||
+      $value == static::formatTime('H:i:s', $time) ||
+      $value == static::formatTime($element['#time_format'], $time)
+    );
+    if (!$is_valid_time) {
       if ($has_access) {
         if (isset($element['#title'])) {
           $form_state->setError($element, t('%name must be a valid time.', ['%name' => $element['#title']]));
@@ -115,7 +142,7 @@ public static function validateWebformTime(&$element, FormStateInterface $form_s
       if ($time < $min) {
         $form_state->setError($element, t('%name must be on or after %min.', [
           '%name' => $name,
-          '%min' => date($time_format, $min),
+          '%min' => static::formatTime($time_format, $min),
         ]));
       }
     }
@@ -126,12 +153,14 @@ public static function validateWebformTime(&$element, FormStateInterface $form_s
       if ($time > $max) {
         $form_state->setError($element, t('%name must be on or before %max.', [
           '%name' => $name,
-          '%max' => date($time_format, $max),
+          '%max' => static::formatTime($time_format, $max),
         ]));
       }
     }
 
-    $value = date('H:i:s', $time);
+    // Convert time to 'H:i:s' format.
+    $value = static::formatTime('H:i:s', $time);
+    $element['#time_format'] = 'H:i:s';
     $element['#value'] = $value;
     $form_state->setValueForElement($element, $value);
   }
@@ -149,17 +178,34 @@ public static function preRenderWebformTime(array $element) {
     if (!empty($element['#timepicker'])) {
       // Render simple text field that is converted to timepicker.
       $element['#attributes']['type'] = 'text';
-      // Apply #time_format to #value.
-      if (!empty($element['#value'])) {
-        $element['#value'] = date($element['#attributes']['data-webform-time-format'], strtotime($element['#value']));
+      // Apply #time_format to #default_value.
+      if (!empty($element['#value']) && !empty($element['#default_value']) && $element['#value'] === $element['#default_value']) {
+        $element['#value'] = static::formatTime($element['#attributes']['data-webform-time-format'], strtotime($element['#value']));
       }
     }
     else {
       $element['#attributes']['type'] = 'time';
     }
-    Element::setAttributes($element, ['id', 'name', 'type', 'value', 'size', 'min', 'max', 'step']);
+    Element::setAttributes($element, ['id', 'name', 'type', 'value', 'size', 'maxlength', 'min', 'max', 'step']);
     static::setAttributes($element, ['form-time', 'webform-time']);
     return $element;
   }
 
+  /**
+   * Format custom time.
+   *
+   * @param string $custom_format
+   *   A PHP date format string suitable for input to date().
+   * @param int $timestamp
+   *   (optional) A UNIX timestamp to format.
+   *
+   * @return string
+   *   Formatted time.
+   */
+  protected static function formatTime($custom_format, $timestamp = NULL) {
+    /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
+    $date_formatter = \Drupal::service('date.formatter');
+    return $date_formatter->format($timestamp ?: time(), 'custom', $custom_format);
+  }
+
 }
diff --git a/web/modules/webform/src/Entity/Webform.php b/web/modules/webform/src/Entity/Webform.php
index deec1a1dcd684ef6058157a14a44599a1d44206f..41534609b0ef713a8de3374ed631716edc683031 100644
--- a/web/modules/webform/src/Entity/Webform.php
+++ b/web/modules/webform/src/Entity/Webform.php
@@ -3,26 +3,24 @@
 namespace Drupal\webform\Entity;
 
 use Drupal\Component\Utility\NestedArray;
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
 use Drupal\Core\Serialization\Yaml;
 use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Render\Element;
-use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\user\Entity\User;
 use Drupal\user\UserInterface;
 use Drupal\webform\Plugin\WebformElement\WebformActions;
-use Drupal\webform\Plugin\WebformElement\WebformManagedFileBase;
 use Drupal\webform\Plugin\WebformElement\WebformWizardPage;
+use Drupal\webform\Plugin\WebformElementAttachmentInterface;
 use Drupal\webform\Plugin\WebformHandlerMessageInterface;
 use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\Utility\WebformReflectionHelper;
 use Drupal\webform\Plugin\WebformHandlerInterface;
 use Drupal\webform\Plugin\WebformHandlerPluginCollection;
+use Drupal\webform\Utility\WebformTextHelper;
+use Drupal\webform\Utility\WebformYaml;
 use Drupal\webform\WebformException;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformSubmissionInterface;
@@ -34,6 +32,13 @@
  * @ConfigEntityType(
  *   id = "webform",
  *   label = @Translation("Webform"),
+ *   label_collection = @Translation("Webforms"),
+ *   label_singular = @Translation("webform"),
+ *   label_plural = @Translation("webforms"),
+ *   label_count = @PluralTranslation(
+ *     singular = "@count webform",
+ *     plural = "@count webforms",
+ *   ),
  *   handlers = {
  *     "storage" = "\Drupal\webform\WebformEntityStorage",
  *     "view_builder" = "Drupal\webform\WebformEntityViewBuilder",
@@ -44,6 +49,7 @@
  *       "duplicate" = "Drupal\webform\WebformEntityAddForm",
  *       "delete" = "Drupal\webform\WebformEntityDeleteForm",
  *       "edit" = "Drupal\webform\WebformEntityElementsForm",
+ *       "export" = "Drupal\webform\WebformEntityExportForm",
  *       "settings" = "Drupal\webform\EntitySettings\WebformEntitySettingsGeneralForm",
  *       "settings_form" = "Drupal\webform\EntitySettings\WebformEntitySettingsFormForm",
  *       "settings_submissions" = "Drupal\webform\EntitySettings\WebformEntitySettingsSubmissionsForm",
@@ -55,12 +61,14 @@
  *   },
  *   admin_permission = "administer webform",
  *   bundle_of = "webform_submission",
+ *   static_cache = TRUE,
  *   entity_keys = {
  *     "id" = "id",
  *     "label" = "title",
  *   },
  *   links = {
  *     "canonical" = "/webform/{webform}",
+ *     "access-denied" = "/webform/{webform}/access-denied",
  *     "submissions" = "/webform/{webform}/submissions",
  *     "add-form" = "/webform/{webform}",
  *     "edit-form" = "/admin/structure/webform/manage/{webform}/elements",
@@ -85,8 +93,10 @@
  *     "status",
  *     "open",
  *     "close",
+ *     "weight",
  *     "uid",
  *     "template",
+ *     "archive",
  *     "id",
  *     "uuid",
  *     "title",
@@ -124,6 +134,13 @@ class Webform extends ConfigEntityBundleBase implements WebformInterface {
    */
   protected $uuid;
 
+  /**
+   * The webform's current operation.
+   *
+   * @var string
+   */
+  protected $operation;
+
   /**
    * The webform override state.
    *
@@ -154,6 +171,13 @@ class Webform extends ConfigEntityBundleBase implements WebformInterface {
    */
   protected $close;
 
+  /**
+   * The webform weight.
+   *
+   * @var int
+   */
+  protected $weight = 0;
+
   /**
    * The webform template indicator.
    *
@@ -161,6 +185,13 @@ class Webform extends ConfigEntityBundleBase implements WebformInterface {
    */
   protected $template = FALSE;
 
+  /**
+   * The webform archive indicator.
+   *
+   * @var bool
+   */
+  protected $archive = FALSE;
+
   /**
    * The webform title.
    *
@@ -316,18 +347,25 @@ class Webform extends ConfigEntityBundleBase implements WebformInterface {
   protected $elementsWizardPages = [];
 
   /**
-   * The webform pages.
+   * Track managed file elements.
    *
    * @var array
    */
-  protected $pages;
+  protected $elementsManagedFiles = [];
 
   /**
-   * Track if the webform has a managed file (upload) element.
+   * Track attachment elements.
    *
-   * @var bool
+   * @var array
    */
-  protected $hasManagedFile = FALSE;
+  protected $elementsAttachments = [];
+
+  /**
+   * The webform pages.
+   *
+   * @var array
+   */
+  protected $pages;
 
   /**
    * Track if the webform is using a flexbox layout.
@@ -378,6 +416,13 @@ public function getLangcode() {
     return $this->langcode;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getWeight() {
+    return $this->weight;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -413,6 +458,29 @@ public function setOwnerId($uid) {
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getOperation() {
+    return $this->operation;
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOperation($operation) {
+    $this->operation = $operation;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isTest() {
+    return ($this->operation === 'test') ? TRUE : FALSE;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -464,6 +532,11 @@ public function status() {
    * {@inheritdoc}
    */
   public function isOpen() {
+    // Archived webforms are always closed.
+    if ($this->isArchived()) {
+      return FALSE;
+    }
+
     switch ($this->status) {
       case WebformInterface::STATUS_OPEN:
         return TRUE;
@@ -515,6 +588,13 @@ public function isTemplate() {
     return $this->template ? TRUE : FALSE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isArchived() {
+    return $this->archive ? TRUE : FALSE;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -522,6 +602,13 @@ public function isConfidential() {
     return $this->getSetting('form_confidential');
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function hasRemoteAddr() {
+    return (!$this->isConfidential() && $this->getSetting('form_remote_addr')) ? TRUE : FALSE;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -588,7 +675,15 @@ public function hasPage() {
    */
   public function hasManagedFile() {
     $this->initElements();
-    return $this->hasManagedFile;
+    return (!empty($this->elementsManagedFiles)) ? TRUE : FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasAttachments() {
+    $this->initElements();
+    return (!empty($this->elementsAttachments)) ? TRUE : FALSE;
   }
 
   /**
@@ -832,6 +927,7 @@ public static function getDefaultSettings() {
       'page' => TRUE,
       'page_submit_path' => '',
       'page_confirm_path' => '',
+      'form_title' => 'both',
       'form_submit_once' => FALSE,
       'form_exception_message' => '',
       'form_open_message' => '',
@@ -839,6 +935,7 @@ public static function getDefaultSettings() {
       'form_previous_submissions' => TRUE,
       'form_confidential' => FALSE,
       'form_confidential_message' => '',
+      'form_remote_addr' => TRUE,
       'form_convert_anonymous' => FALSE,
       'form_prepopulate' => FALSE,
       'form_prepopulate_source_entity' => FALSE,
@@ -854,22 +951,37 @@ public static function getDefaultSettings() {
       'form_submit_back' => FALSE,
       'form_autofocus' => FALSE,
       'form_details_toggle' => FALSE,
-      'form_login' => FALSE,
-      'form_login_message' => '',
+      'form_access_denied' => WebformInterface::ACCESS_DENIED_DEFAULT,
+      'form_access_denied_title' => '',
+      'form_access_denied_message' => '',
+      'form_access_denied_attributes' => [],
+      'form_file_limit' => '',
       'submission_label' => '',
       'submission_log' => FALSE,
+      'submission_views' => [],
+      'submission_views_replace' => [],
       'submission_user_columns' => [],
-      'submission_login' => FALSE,
-      'submission_login_message' => '',
+      'submission_user_duplicate' => FALSE,
+      'submission_access_denied' => WebformInterface::ACCESS_DENIED_DEFAULT,
+      'submission_access_denied_title' => '',
+      'submission_access_denied_message' => '',
+      'submission_access_denied_attributes' => [],
       'submission_exception_message' => '',
       'submission_locked_message' => '',
+      'submission_excluded_elements' => [],
+      'submission_exclude_empty' => FALSE,
+      'submission_exclude_empty_checkbox' => FALSE,
+      'previous_submission_message' => '',
+      'previous_submissions_message' => '',
       'autofill' => FALSE,
       'autofill_message' => '',
       'autofill_excluded_elements' => [],
       'wizard_progress_bar' => TRUE,
       'wizard_progress_pages' => FALSE,
       'wizard_progress_percentage' => FALSE,
+      'wizard_progress_link' => FALSE,
       'wizard_start_label' => '',
+      'wizard_preview_link' => FALSE,
       'wizard_confirmation' => TRUE,
       'wizard_confirmation_label' => '',
       'wizard_track' => '',
@@ -880,6 +992,7 @@ public static function getDefaultSettings() {
       'preview_attributes' => [],
       'preview_excluded_elements' => [],
       'preview_exclude_empty' => TRUE,
+      'preview_exclude_empty_checkbox' => FALSE,
       'draft' => self::DRAFT_NONE,
       'draft_multiple' => FALSE,
       'draft_auto_save' => FALSE,
@@ -898,9 +1011,11 @@ public static function getDefaultSettings() {
       'limit_total' => NULL,
       'limit_total_interval' => NULL,
       'limit_total_message' => '',
+      'limit_total_unique' => FALSE,
       'limit_user' => NULL,
       'limit_user_interval' => NULL,
       'limit_user_message' => '',
+      'limit_user_unique' => FALSE,
       'entity_limit_total' => NULL,
       'entity_limit_total_interval' => NULL,
       'entity_limit_user' => NULL,
@@ -913,176 +1028,6 @@ public static function getDefaultSettings() {
     ];
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function getDefaultAccessRules() {
-    return [
-      'create' => [
-        'roles' => [
-          'anonymous',
-          'authenticated',
-        ],
-        'users' => [],
-        'permissions' => [],
-      ],
-      'view_any' => [
-        'roles' => [],
-        'users' => [],
-        'permissions' => [],
-      ],
-      'update_any' => [
-        'roles' => [],
-        'users' => [],
-        'permissions' => [],
-      ],
-      'delete_any' => [
-        'roles' => [],
-        'users' => [],
-        'permissions' => [],
-      ],
-      'purge_any' => [
-        'roles' => [],
-        'users' => [],
-        'permissions' => [],
-      ],
-      'view_own' => [
-        'roles' => [],
-        'users' => [],
-        'permissions' => [],
-      ],
-      'update_own' => [
-        'roles' => [],
-        'users' => [],
-        'permissions' => [],
-      ],
-      'delete_own' => [
-        'roles' => [],
-        'users' => [],
-        'permissions' => [],
-      ],
-      'administer' => [
-        'roles' => [],
-        'users' => [],
-        'permissions' => [],
-      ],
-      'test' => [
-        'roles' => [],
-        'users' => [],
-        'permissions' => [],
-      ],
-    ];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function checkAccessRules($operation, AccountInterface $account, WebformSubmissionInterface $webform_submission = NULL) {
-    // Always grant access to user that can administer webforms.
-    if ($account->hasPermission('administer webform')) {
-      return AccessResult::allowed()->cachePerPermissions();
-    }
-
-    // Grant user with administer webform submission access to view all webform submissions.
-    if ($account->hasPermission('administer webform submission') && $operation != 'administer') {
-      return AccessResult::allowed()->cachePerPermissions();
-    }
-
-    // The "page" operation is the same as "create" but requires that the
-    // Webform is allowed to be displayed as dedicated page.
-    // Used by the 'entity.webform.canonical' route.
-    if ($operation == 'page') {
-      if (empty($this->settings['page'])) {
-        return AccessResult::forbidden()->addCacheableDependency($this);
-      }
-      else {
-        $operation = 'create';
-      }
-    }
-
-    $access_rules = $this->getAccessRules() + static::getDefaultAccessRules();
-
-    $cacheability = new CacheableMetadata();
-    $cacheability->addCacheableDependency($this);
-    $cacheability->addCacheContexts(['user.permissions']);
-    foreach ($access_rules as $access_rule) {
-      // If there is some per-user access logic, our response must be cacheable
-      // accordingly.
-      if (!empty($access_rule['users'])) {
-        $cacheability->addCacheContexts(['user']);
-      }
-    }
-
-    // Check administer access rule and grant full access to user.
-    if ($this->checkAccessRule($access_rules['administer'], $account)) {
-      return AccessResult::allowed()->addCacheableDependency($cacheability);
-    }
-
-    // Check operation specific access rules.
-    if (in_array($operation, ['create', 'view_any', 'update_any', 'delete_any', 'purge_any', 'administer', 'test'])
-      && $this->checkAccessRule($access_rules[$operation], $account)) {
-      return AccessResult::allowed()->addCacheableDependency($cacheability);
-    }
-    if (isset($access_rules[$operation . '_any'])
-      && $this->checkAccessRule($access_rules[$operation . '_any'], $account)) {
-      return AccessResult::allowed()->addCacheableDependency($cacheability);
-    }
-
-    // If webform submission is not set then check 'view own'.
-    // @see \Drupal\webform\WebformSubmissionForm::displayMessages.
-    if (empty($webform_submission)
-      && $operation === 'view_own'
-      && $this->checkAccessRule($access_rules[$operation], $account)) {
-      return AccessResult::allowed()->addCacheableDependency($cacheability);
-    }
-
-    // If webform submission is set then check the webform submission owner.
-    if (!empty($webform_submission)) {
-      $is_authenticated_owner = ($account->isAuthenticated() && $account->id() === $webform_submission->getOwnerId());
-      $is_anonymous_owner = ($account->isAnonymous() && !empty($_SESSION['webform_submissions']) && isset($_SESSION['webform_submissions'][$webform_submission->id()]));
-      $is_owner = ($is_authenticated_owner || $is_anonymous_owner);
-      if ($is_owner) {
-        if (isset($access_rules[$operation . '_own'])
-          && $this->checkAccessRule($access_rules[$operation . '_own'], $account)) {
-          return AccessResult::allowed()->cachePerUser()->addCacheableDependency($cacheability);
-        }
-      }
-    }
-
-    return AccessResult::forbidden()->addCacheableDependency($cacheability);
-  }
-
-  /**
-   * Checks an access rule against a user account's roles and id.
-   *
-   * @param array $access_rule
-   *   An access rule.
-   * @param \Drupal\Core\Session\AccountInterface $account
-   *   The user session for which to check access.
-   *
-   * @return bool
-   *   The access result. Returns a TRUE if access is allowed.
-   *
-   * @see \Drupal\webform\Plugin\WebformElementBase::checkAccessRule
-   */
-  protected function checkAccessRule(array $access_rule, AccountInterface $account) {
-    if (!empty($access_rule['roles']) && array_intersect($access_rule['roles'], $account->getRoles())) {
-      return TRUE;
-    }
-    elseif (!empty($access_rule['users']) && in_array($account->id(), $access_rule['users'])) {
-      return TRUE;
-    }
-    elseif (!empty($access_rule['permissions'])) {
-      foreach ($access_rule['permissions'] as $permission) {
-        if ($account->hasPermission($permission)) {
-          return TRUE;
-        }
-      }
-    }
-
-    return FALSE;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -1176,6 +1121,22 @@ public function getElementsInitializedFlattenedAndHasValue($operation = NULL) {
     return $this->checkElementsFlattenedAccess($operation, $this->elementsInitializedFlattenedAndHasValue);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementsManagedFiles() {
+    $this->initElements();
+    return $this->elementsManagedFiles;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementsAttachments() {
+    $this->initElements();
+    return $this->elementsAttachments;
+  }
+
   /**
    * Check operation access for each element.
    *
@@ -1215,11 +1176,43 @@ public function getElementsSelectorOptions() {
     $elements = $this->getElementsInitializedAndFlattened();
     foreach ($elements as $element) {
       $element_plugin = $element_manager->getElementInstance($element);
+      $element_selectors = $element_plugin->getElementSelectorOptions($element);
+      foreach ($element_selectors as $element_selector_key => $element_selector_value) {
+        // Suffix duplicate selector optgroups with empty characters.
+        //
+        // This prevents elements with the same titles and multiple selectors
+        // from clobbering each other.
+        if (isset($selectors[$element_selector_key]) && is_array($element_selector_value)) {
+          while (isset($selectors[$element_selector_key])) {
+            $element_selector_key .= ' ';
+          }
+          $selectors[$element_selector_key] = $element_selector_value;
+        }
+        else {
+          $selectors[$element_selector_key] = $element_selector_value;
+        }
+      }
       $selectors += $element_plugin->getElementSelectorOptions($element);
     }
     return $selectors;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementsSelectorSourceValues() {
+    /** @var \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager */
+    $element_manager = \Drupal::service('plugin.manager.webform.element');
+
+    $source_values = [];
+    $elements = $this->getElementsInitializedAndFlattened();
+    foreach ($elements as $element) {
+      $element_plugin = $element_manager->getElementInstance($element);
+      $source_values += $element_plugin->getElementSelectorSourceValues($element);
+    }
+    return $source_values;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -1231,7 +1224,7 @@ public function getElementsPrepopulate() {
    * {@inheritdoc}
    */
   public function setElements(array $elements) {
-    $this->elements = Yaml::encode($elements);
+    $this->elements = WebformYaml::encode($elements);
     $this->resetElements();
     return $this;
   }
@@ -1245,7 +1238,6 @@ protected function initElements() {
     }
 
     // @see \Drupal\webform\Entity\Webform::resetElements
-    $this->hasManagedFile = FALSE;
     $this->hasFlexboxLayout = FALSE;
     $this->hasContainer = FALSE;
     $this->hasConditions = FALSE;
@@ -1257,6 +1249,8 @@ protected function initElements() {
     $this->elementsInitializedAndFlattened = [];
     $this->elementsInitializedFlattenedAndHasValue = [];
     $this->elementsTranslations = [];
+    $this->elementsManagedFiles = [];
+    $this->elementsAttachments = [];
 
     try {
       $config_translation = \Drupal::moduleHandler()->moduleExists('config_translation');
@@ -1265,11 +1259,16 @@ protected function initElements() {
       /** @var \Drupal\Core\Language\LanguageManagerInterface $language_manager */
       $language_manager = \Drupal::service('language_manager');
 
-      // If current webform is translated, load the base (default) webform and apply
-      // the translation to the elements.
-      if ($config_translation && $this->langcode != $language_manager->getCurrentLanguage()->getId()) {
+      // If current webform is translated, load the base (default) webform and
+      // apply the translation to the elements.
+      if ($config_translation
+        && ($this->langcode != $language_manager->getCurrentLanguage()->getId())) {
+        // Always get the elements in the original language.
         $elements = $translation_manager->getElements($this);
-        $this->elementsTranslations = $translation_manager->getElements($this, $language_manager->getCurrentLanguage()->getId());
+        // For none admin routes get the element (label) translations.
+        if (!$translation_manager->isAdminRoute()) {
+          $this->elementsTranslations = $translation_manager->getElements($this, $language_manager->getCurrentLanguage()->getId());
+        }
       }
       else {
         $elements = Yaml::decode($this->elements);
@@ -1303,7 +1302,6 @@ protected function initElements() {
    */
   protected function resetElements() {
     $this->pages = NULL;
-    $this->hasManagedFile = NULL;
     $this->hasFlexboxLayout = NULL;
     $this->hasContainer = NULL;
     $this->hasConditions = NULL;
@@ -1317,6 +1315,8 @@ protected function resetElements() {
     $this->elementsInitializedAndFlattened = NULL;
     $this->elementsInitializedFlattenedAndHasValue = NULL;
     $this->elementsTranslations = NULL;
+    $this->elementsManagedFiles = [];
+    $this->elementsAttachments = [];
   }
 
   /**
@@ -1337,7 +1337,7 @@ protected function initElementsRecursive(array &$elements, $parent = '', $depth
     $elements = WebformElementHelper::removeIgnoredProperties($elements);
 
     foreach ($elements as $key => &$element) {
-      if (Element::property($key) || !is_array($element)) {
+      if (!WebformElementHelper::isElement($element, $key)) {
         continue;
       }
 
@@ -1346,6 +1346,15 @@ protected function initElementsRecursive(array &$elements, $parent = '', $depth
         WebformElementHelper::applyTranslation($element, $this->elementsTranslations[$key]);
       }
 
+      // Prevent regressions where webform_computed_* element is still using
+      // #value instead of #template.
+      if (isset($element['#type']) && strpos($element['#type'], 'webform_computed_') === 0) {
+        if (isset($element['#value']) && !isset($element['#template'])) {
+          $element['#template'] = $element['#value'];
+          unset($element['#value']);
+        }
+      }
+
       // Copy only the element properties to decoded and flattened elements.
       $this->elementsDecodedAndFlattened[$key] = WebformElementHelper::getProperties($element);
 
@@ -1403,11 +1412,6 @@ protected function initElementsRecursive(array &$elements, $parent = '', $depth
         // and stored in the '#webform_composite_elements' property.
         $element_plugin->initialize($element);
 
-        // Track managed file upload.
-        if ($element_plugin instanceof WebformManagedFileBase) {
-          $this->hasManagedFile = TRUE;
-        }
-
         // Track flexbox.
         if ($element['#type'] == 'flexbox' || $element['#type'] == 'webform_flexbox') {
           $this->hasFlexboxLayout = TRUE;
@@ -1443,6 +1447,16 @@ protected function initElementsRecursive(array &$elements, $parent = '', $depth
           $this->elementsWizardPages[$key] = $key;
         }
 
+        // Track managed files.
+        if ($element_plugin->hasManagedFiles($element)) {
+          $this->elementsManagedFiles[$key] = $key;
+        }
+
+        // Track attachments.
+        if ($element_plugin instanceof WebformElementAttachmentInterface) {
+          $this->elementsAttachments[$key] = $key;
+        }
+
         $element['#webform_multiple'] = $element_plugin->hasMultipleValues($element);
         $element['#webform_composite'] = $element_plugin->isComposite();
       }
@@ -1537,7 +1551,7 @@ public function setElementProperties($key, array $properties, $parent_key = '')
    */
   protected function setElementPropertiesRecursive(array &$elements, $key, array $properties, $parent_key = '') {
     foreach ($elements as $element_key => &$element) {
-      if (Element::property($element_key) || !is_array($element)) {
+      if (!WebformElementHelper::isElement($element, $element_key)) {
         continue;
       }
 
@@ -1589,7 +1603,7 @@ public function deleteElement($key) {
    */
   protected function deleteElementRecursive(array &$elements, $key) {
     foreach ($elements as $element_key => &$element) {
-      if (Element::property($element_key) || !is_array($element)) {
+      if (!WebformElementHelper::isElement($element, $element_key)) {
         continue;
       }
 
@@ -1618,7 +1632,7 @@ protected function deleteElementRecursive(array &$elements, $key) {
    */
   protected function collectSubElementKeysRecursive(array &$sub_element_keys, array $elements) {
     foreach ($elements as $key => &$element) {
-      if (Element::property($key) || !is_array($element)) {
+      if (!WebformElementHelper::isElement($element, $key)) {
         continue;
       }
       $sub_element_keys[$key] = $key;
@@ -1717,13 +1731,23 @@ public function createDuplicate() {
     // Update owner to current user.
     $duplicate->setOwnerId(\Drupal::currentUser()->id());
 
-    // If template, clear description, remove template flag, and publish.
-    if ($duplicate->isTemplate()) {
+    // If template being used to create a new webform, clear description
+    // and remove the template flag.
+    // @see \Drupal\webform_templates\Controller\WebformTemplatesController::index
+    $is_template_duplicate = \Drupal::request()->get('template');
+    if ($duplicate->isTemplate() && !$is_template_duplicate) {
       $duplicate->set('description', '');
       $duplicate->set('template', FALSE);
-      $duplicate->setStatus(TRUE);
     }
 
+    // If archived, remove archive flag.
+    if ($duplicate->isArchived()) {
+      $duplicate->set('archive', FALSE);
+    }
+
+    // Set default status.
+    $duplicate->setStatus(\Drupal::config('webform.settings')->get('settings.default_status'));
+
     // Remove enforce module dependency when a sub-module's webform is
     // duplicated.
     if (isset($duplicate->dependencies['enforced']['module'])) {
@@ -1744,11 +1768,14 @@ public function createDuplicate() {
    * {@inheritdoc}
    */
   public static function preCreate(EntityStorageInterface $storage, array &$values) {
+    /** @var \Drupal\webform\WebformAccessRulesManagerInterface $access_rules_manager */
+    $access_rules_manager = \Drupal::service('webform.access_rules_manager');
+
     $values += [
-      'status' => WebformInterface::STATUS_OPEN,
+      'status' => \Drupal::config('webform.settings')->get('settings.default_status'),
       'uid' => \Drupal::currentUser()->id(),
       'settings' => static::getDefaultSettings(),
-      'access' => static::getDefaultAccessRules(),
+      'access' => $access_rules_manager->getDefaultAccessRules(),
     ];
 
     // Convert boolean status to STATUS constant.
@@ -1791,6 +1818,7 @@ public static function preDelete(EntityStorageInterface $storage, array $entitie
 
     // Delete all submission associated with this webform.
     $submission_ids = \Drupal::entityQuery('webform_submission')
+      ->accessCheck(FALSE)
       ->condition('webform_id', array_keys($entities), 'IN')
       ->sort('sid')
       ->execute();
@@ -1841,6 +1869,23 @@ public function getCacheContexts() {
     return $cache_contexts;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    if ($this->isScheduled()) {
+      $time = time();
+      if ($this->open && strtotime($this->open) > $time) {
+        return (strtotime($this->open) - $time);
+      }
+      elseif ($this->close && strtotime($this->close) > $time) {
+        return (strtotime($this->close) - $time);
+      }
+    }
+
+    return parent::getCacheMaxAge();
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -1935,37 +1980,37 @@ public function updatePaths() {
       return;
     }
 
-    $submit_base_path = '/' . ($page_submit_path ?: $default_page_base_path . '/' . str_replace('_', '-', $this->id()));
-
-    // Update submit path.
-    $submit_suffixes = [
-      '',
-      '/submissions',
-      '/drafts',
-    ];
-    foreach ($submit_suffixes as $submit_suffix) {
-      $submit_source = '/webform/' . $this->id() . $submit_suffix;
-      $submit_alias = $submit_base_path . $submit_suffix;
-      $this->updatePath($submit_source, $submit_alias, $this->langcode);
-      $this->updatePath($submit_source, $submit_alias, LanguageInterface::LANGCODE_NOT_SPECIFIED);
+    // Update webform base, confirmation, submissions and drafts paths.
+    $path_base_alias = '/' . ($page_submit_path ?: $default_page_base_path . '/' . str_replace('_', '-', $this->id()));
+    $path_suffixes = ['', '/confirmation', '/submissions', '/drafts'];
+    foreach ($path_suffixes as $path_suffix) {
+      $path_source = '/webform/' . $this->id() . $path_suffix;
+      $path_alias = $path_base_alias . $path_suffix;
+      if ($path_suffix === '/confirmation' && $this->settings['page_confirm_path']) {
+        $path_alias = '/' . trim($this->settings['page_confirm_path'], '/');
+      }
+      $this->updatePath($path_source, $path_alias, $this->langcode);
+      $this->updatePath($path_source, $path_alias, LanguageInterface::LANGCODE_NOT_SPECIFIED);
     }
-
-    // Update confirm path.
-    $confirm_source = '/webform/' . $this->id() . '/confirmation';
-    $confirm_alias = $this->settings['page_confirm_path'] ?:  $submit_base_path . '/confirmation';
-    $confirm_alias = '/' . trim($confirm_alias, '/');
-    $this->updatePath($confirm_source, $confirm_alias, $this->langcode);
-    $this->updatePath($confirm_source, $confirm_alias, LanguageInterface::LANGCODE_NOT_SPECIFIED);
   }
 
   /**
    * {@inheritdoc}
    */
   public function deletePaths() {
+    // Path module must be enabled for URL aliases to be updated.
+    if (!\Drupal::moduleHandler()->moduleExists('path')) {
+      return;
+    }
+
     /** @var \Drupal\Core\Path\AliasStorageInterface $path_alias_storage */
     $path_alias_storage = \Drupal::service('path.alias_storage');
-    $path_alias_storage->delete(['source' => '/webform/' . $this->id()]);
-    $path_alias_storage->delete(['source' => '/webform/' . $this->id() . '/confirmation']);
+
+    // Delete webform base, confirmation, submissions and drafts paths.
+    $path_suffixes = ['', '/confirmation', '/submissions', '/drafts'];
+    foreach ($path_suffixes as $path_suffix) {
+      $path_alias_storage->delete(['source' => '/webform/' . $this->id() . $path_suffix]);
+    }
   }
 
   /**
@@ -2172,6 +2217,9 @@ public function invokeHandlers($method, &$data, &$context1 = NULL, &$context2 =
       $settings = $this->getSettings();
       $handlers = $this->getHandlers();
       foreach ($handlers as $handler) {
+        $handler->setWebformSubmission($webform_submission);
+        $this->invokeHandlerAlter($handler, $method, $args);
+
         if ($handler->isEnabled() && $handler->checkConditions($webform_submission)) {
           $handler->overrideSettings($settings, $webform_submission);
         }
@@ -2183,6 +2231,9 @@ public function invokeHandlers($method, &$data, &$context1 = NULL, &$context2 =
     else {
       $handlers = $this->getHandlers();
       foreach ($handlers as $handler) {
+        $handler->setWebformSubmission($webform_submission);
+        $this->invokeHandlerAlter($handler, $method, $args);
+
         // If the handler is disabled never invoke it.
         if ($handler->isDisabled()) {
           continue;
@@ -2198,6 +2249,25 @@ public function invokeHandlers($method, &$data, &$context1 = NULL, &$context2 =
     }
   }
 
+  /**
+   * Alter a webform handler when it is invoked.
+   *
+   * @param \Drupal\webform\Plugin\WebformHandlerInterface $handler
+   *   A webform handler.
+   * @param string $method_name
+   *   The handler method to be invoked.
+   * @param array $args
+   *   Array of arguments being passed to the handler's method.
+   *
+   * @see hook_webform_handler_invoke_alter()
+   * @see hook_webform_handler_invoke_METHOD_NAME_alter()
+   */
+  protected function invokeHandlerAlter(WebformHandlerInterface $handler, $method_name, array $args) {
+    $method_name = WebformTextHelper::camelToSnake($method_name);
+    \Drupal::moduleHandler()->alter('webform_handler_invoke', $handler, $method_name, $args);
+    \Drupal::moduleHandler()->alter('webform_handler_invoke_' . $method_name, $handler, $args);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/web/modules/webform/src/Entity/WebformOptions.php b/web/modules/webform/src/Entity/WebformOptions.php
index 9af18274ee543108df1790abd5c43d58b7acae64..c2c4bd6d2c84ca2947a9b7ee4d7b7223c643112c 100644
--- a/web/modules/webform/src/Entity/WebformOptions.php
+++ b/web/modules/webform/src/Entity/WebformOptions.php
@@ -16,6 +16,13 @@
  * @ConfigEntityType(
  *   id = "webform_options",
  *   label = @Translation("Webform options"),
+ *   label_collection = @Translation("Webform options"),
+ *   label_singular = @Translation("webform options"),
+ *   label_plural = @Translation("webform options"),
+ *   label_count = @PluralTranslation(
+ *     singular = "@count webform options",
+ *     plural = "@count webform options",
+ *   ),
  *   handlers = {
  *     "storage" = "\Drupal\webform\WebformOptionsStorage",
  *     "access" = "Drupal\webform\WebformOptionsAccessControlHandler",
@@ -135,6 +142,7 @@ public function getOptions() {
   public function setOptions(array $options) {
     $this->options = Yaml::encode($options);
     $this->optionsDecoded = NULL;
+    return $this;
   }
 
   /**
@@ -186,11 +194,12 @@ public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b)
    * {@inheritdoc}
    */
   public static function getElementOptions(array &$element, $property_name = '#options') {
-    // If element already has #options return them.
-    // NOTE: Only WebformOptions can be altered. If you need to alter an
-    // element's options, @see hook_webform_element_alter().
+    // If element already has #options array just call alter hook with
+    // a NULL id.
     if (is_array($element[$property_name])) {
-      return $element[$property_name];
+      $options = $element[$property_name];
+      \Drupal::moduleHandler()->alter('webform_options', $options, $element);
+      return $options;
     }
 
     // Return empty options if element does not define an options id.
diff --git a/web/modules/webform/src/Entity/WebformSubmission.php b/web/modules/webform/src/Entity/WebformSubmission.php
index 342afda2afbaf56d3d59d6b39ac80277ae49d463..7dffefc5af7650662c2803d84c96353e0b0a0da3 100644
--- a/web/modules/webform/src/Entity/WebformSubmission.php
+++ b/web/modules/webform/src/Entity/WebformSubmission.php
@@ -3,6 +3,7 @@
 namespace Drupal\webform\Entity;
 
 use Drupal\Component\Render\PlainTextOutput;
+use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Serialization\Yaml;
 use Drupal\Component\Utility\Crypt;
 use Drupal\Core\Entity\EntityStorageInterface;
@@ -10,6 +11,7 @@
 use Drupal\Core\Entity\ContentEntityBase;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityChangedTrait;
+use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\user\Entity\User;
 use Drupal\user\UserInterface;
@@ -24,6 +26,13 @@
  * @ContentEntityType(
  *   id = "webform_submission",
  *   label = @Translation("Webform submission"),
+ *   label_collection = @Translation("Submissions"),
+ *   label_singular = @Translation("submission"),
+ *   label_plural = @Translation("submissions"),
+ *   label_count = @PluralTranslation(
+ *     singular = "@count submission",
+ *     plural = "@count submissions",
+ *   ),
  *   bundle_label = @Translation("Webform"),
  *   handlers = {
  *     "storage" = "Drupal\webform\WebformSubmissionStorage",
@@ -45,6 +54,7 @@
  *   },
  *   bundle_entity_type = "webform",
  *   list_cache_contexts = { "user" },
+ *   list_cache_tags = { "config:webform_list", "webform_submission_list" },
  *   base_table = "webform_submission",
  *   admin_permission = "administer webform",
  *   entity_keys = {
@@ -55,10 +65,10 @@
  *   },
  *   links = {
  *     "canonical" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}",
+ *     "access-denied" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/access-denied",
  *     "table" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/table",
  *     "text" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/text",
  *     "yaml" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/yaml",
- *     "yaml" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/yaml",
  *     "edit-form" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/edit",
  *     "notes-form" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/notes",
  *     "resend-form" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/resend",
@@ -185,7 +195,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
 
     // Can't use entity reference without a target type because it defaults to
     // an integer which limits reference to only content entities (and not
-    // config entities like Views, Panels, etc...).
+    // config entities like Views, Panels, etc…).
     // @see \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::propertyDefinitions()
     $fields['entity_id'] = BaseFieldDefinition::create('string')
       ->setLabel(t('Submitted to: Entity ID'))
@@ -223,7 +233,10 @@ public function serial() {
   public function label() {
     $submission_label = $this->getWebform()->getSetting('submission_label')
       ?: \Drupal::config('webform.settings')->get('settings.default_submission_label');
-    return PlainTextOutput::renderFromHtml(\Drupal::service('webform.token_manager')->replace($submission_label, $this));
+
+    /** @var \Drupal\webform\WebformTokenManagerInterface $token_manager */
+    $token_manager = \Drupal::service('webform.token_manager');
+    return PlainTextOutput::renderFromHtml($token_manager->replaceNoRenderContext($submission_label, $this));
   }
 
   /**
@@ -421,11 +434,20 @@ public function getWebform() {
   /**
    * {@inheritdoc}
    */
-  public function getSourceEntity() {
+  public function getSourceEntity($translate = FALSE) {
     if ($this->entity_type->value && $this->entity_id->value) {
       $entity_type = $this->entity_type->value;
       $entity_id = $this->entity_id->value;
-      return $this->entityTypeManager()->getStorage($entity_type)->load($entity_id);
+      $source_entity = $this->entityTypeManager()->getStorage($entity_type)->load($entity_id);
+
+      // If translated is set, get the translated source entity.
+      if ($translate && $source_entity instanceof ContentEntityInterface) {
+        $langcode = $this->language()->getId();
+        if ($source_entity->hasTranslation($langcode)) {
+          $source_entity = $source_entity->getTranslation($langcode);
+        }
+      }
+      return $source_entity;
     }
     else {
       return NULL;
@@ -452,8 +474,10 @@ public function getSourceUrl() {
    * {@inheritdoc}
    */
   public function getTokenUrl() {
-    return $this->getSourceUrl()
-      ->setOption('query', ['token' => $this->token->value]);
+    $uri = $this->getSourceUrl();
+    $options = $uri->getOptions();
+    $options['query']['token'] = $this->getToken();
+    return $uri->setOptions($options);
   }
 
   /**
@@ -543,6 +567,18 @@ public function isSticky() {
     return (bool) $this->get('sticky')->value;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isOwner(AccountInterface $account) {
+    if ($account->isAnonymous()) {
+      return !empty($_SESSION['webform_submissions']) && isset($_SESSION['webform_submissions'][$this->id()]);
+    }
+    else {
+      return $account->id() === $this->getOwnerId();
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -625,7 +661,7 @@ public static function preCreate(EntityStorageInterface $storage, array &$values
 
     // Get temporary webform entity and store it in the static
     // WebformSubmission::$webform property.
-    // This could be reworked to use \Drupal\user\PrivateTempStoreFactory
+    // This could be reworked to use \Drupal\Core\TempStore\PrivateTempStoreFactory
     // but it might be overkill since we are just using this to validate
     // that a webform's elements can be rendered.
     // @see \Drupal\webform\WebformEntityElementsValidator::validateRendering()
@@ -682,7 +718,7 @@ public static function preCreate(EntityStorageInterface $storage, array &$values
       'langcode' => \Drupal::languageManager()->getCurrentLanguage()->getId(),
       'token' => Crypt::randomBytesBase64(),
       'uri' => preg_replace('#^' . base_path() . '#', '/', $current_request->getRequestUri()),
-      'remote_addr' => ($webform && $webform->isConfidential()) ? '' : $current_request->getClientIp(),
+      'remote_addr' => ($webform && $webform->hasRemoteAddr()) ? '' : $current_request->getClientIp(),
     ];
 
     $webform->invokeHandlers(__FUNCTION__, $values);
@@ -693,20 +729,31 @@ public static function preCreate(EntityStorageInterface $storage, array &$values
    * {@inheritdoc}
    */
   public function preSave(EntityStorageInterface $storage) {
+    // Because the original data is not stored in the persistent entity storage
+    // cache we have to reset it for the original webform submission entity.
+    // @see \Drupal\Core\Entity\EntityStorageBase::doPreSave
+    // @see \Drupal\Core\Entity\ContentEntityStorageBase::getFromPersistentCache
+    if (isset($this->original)) {
+      $this->original->setData($this->originalData);
+      $this->original->setOriginalData($this->originalData);
+    }
+
+    $request_time = \Drupal::time()->getRequestTime();
+
     // Set created.
     if (!$this->created->value) {
-      $this->created->value = REQUEST_TIME;
+      $this->created->value = $request_time;
     }
 
     // Set changed.
-    $this->changed->value = REQUEST_TIME;
+    $this->changed->value = $request_time;
 
     // Set completed.
     if ($this->isDraft()) {
       $this->completed->value = NULL;
     }
     elseif (!$this->isCompleted()) {
-      $this->completed->value = REQUEST_TIME;
+      $this->completed->value = $request_time;
     }
 
     parent::preSave($storage);
@@ -724,7 +771,7 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
    */
   public function save() {
     // Clear the remote_addr for confidential submissions.
-    if ($this->getWebform()->isConfidential()) {
+    if (!$this->getWebform()->hasRemoteAddr()) {
       $this->get('remote_addr')->value = '';
     }
 
diff --git a/web/modules/webform/src/EntitySettings/WebformEntitySettingsAccessForm.php b/web/modules/webform/src/EntitySettings/WebformEntitySettingsAccessForm.php
index 133929c6f0e9b6474b7c02a1d2f506dfe62d8c21..8317a9a6613fac5ca86e19deede04183a9e89363 100644
--- a/web/modules/webform/src/EntitySettings/WebformEntitySettingsAccessForm.php
+++ b/web/modules/webform/src/EntitySettings/WebformEntitySettingsAccessForm.php
@@ -3,12 +3,40 @@
 namespace Drupal\webform\EntitySettings;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\WebformAccessRulesManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Webform access settings.
  */
 class WebformEntitySettingsAccessForm extends WebformEntitySettingsBaseForm {
 
+  /**
+   * Webform access rules manager.
+   *
+   * @var \Drupal\webform\WebformAccessRulesManagerInterface
+   */
+  protected $accessRulesManager;
+
+  /**
+   * WebformEntitySettingsAccessForm constructor.
+   *
+   * @param \Drupal\webform\WebformAccessRulesManagerInterface $access_rules_manager
+   *   Webform access rules manager.
+   */
+  public function __construct(WebformAccessRulesManagerInterface $access_rules_manager) {
+    $this->accessRulesManager = $access_rules_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('webform.access_rules_manager')
+    );
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -16,54 +44,40 @@ public function form(array $form, FormStateInterface $form_state) {
     /** @var \Drupal\webform\WebformInterface $webform */
     $webform = $this->entity;
 
-    $access = $webform->getAccessRules();
-    $permissions = [
-      'create' => $this->t('Create webform submissions'),
-      'view_any' => $this->t('View all webform submissions'),
-      'update_any' => $this->t('Update all webform submissions'),
-      'delete_any' => $this->t('Delete all webform submissions'),
-      'purge_any' => $this->t('Purge all webform submissions'),
-      'view_own' => $this->t('View own webform submissions'),
-      'update_own' => $this->t('Update own webform submissions'),
-      'delete_own' => $this->t('Delete own webform submissions'),
-      'administer' => $this->t('Administer webform &amp; submissions'),
-      'test' => $this->t('Test webform'),
-    ];
-
     $form['access']['#tree'] = TRUE;
-    foreach ($permissions as $name => $title) {
-      $form['access'][$name] = [
-        '#type' => ($name === 'create') ? 'fieldset' : 'details',
-        '#title' => $title,
-        '#open' => ($access[$name]['roles'] || $access[$name]['users']) ? TRUE : FALSE,
+
+    $access = $webform->getAccessRules() + $this->accessRulesManager->getDefaultAccessRules();
+    $access_rules = $this->accessRulesManager->getAccessRulesInfo();
+    foreach ($access_rules as $access_rule => $info) {
+      $form['access'][$access_rule] = [
+        '#type' => ($access_rule === 'create') ? 'fieldset' : 'details',
+        '#title' => $info['title'],
+        '#open' => ($access[$access_rule]['roles'] || $access[$access_rule]['users']) ? TRUE : FALSE,
+        '#description' => $info['description'],
+        // Never convert description to help.
+        // @see _webform_preprocess_description_help()
+        '#help' => FALSE,
       ];
-      $form['access'][$name]['roles'] = [
+      $form['access'][$access_rule]['roles'] = [
         '#type' => 'webform_roles',
         '#title' => $this->t('Roles'),
-        '#include_anonymous' => (!in_array($name, ['update_any', 'delete_any', 'purge_any'])) ? TRUE : FALSE,
-        '#default_value' => $access[$name]['roles'],
+        '#include_anonymous' => (!in_array($access_rule, ['update_any', 'delete_any', 'purge_any'])) ? TRUE : FALSE,
+        '#default_value' => $access[$access_rule]['roles'],
       ];
-      $form['access'][$name]['users'] = [
+      $form['access'][$access_rule]['users'] = [
         '#type' => 'webform_users',
         '#title' => $this->t('Users'),
-        '#default_value' => $access[$name]['users'] ? $this->entityTypeManager->getStorage('user')->loadMultiple($access[$name]['users']) : [],
+        '#default_value' => $access[$access_rule]['users'] ? $this->entityTypeManager->getStorage('user')->loadMultiple($access[$access_rule]['users']) : [],
       ];
-      $form['access'][$name]['permissions'] = [
+      $form['access'][$access_rule]['permissions'] = [
         '#type' => 'webform_permissions',
         '#title' => $this->t('Permissions'),
         '#multiple' => TRUE,
         '#select2' => TRUE,
-        '#default_value' => $access[$name]['permissions'],
+        '#default_value' => $access[$access_rule]['permissions'],
       ];
     }
 
-    $form['access']['administer']['message'] = [
-      '#weight' => -10,
-      '#type' => 'webform_message',
-      '#message_type' => 'warning',
-      '#message_message' => $this->t('<strong>Warning</strong>: The below settings give the below users, permissions, and roles full access to this webform and its submissions.'),
-    ];
-
     return parent::form($form, $form_state);
   }
 
diff --git a/web/modules/webform/src/EntitySettings/WebformEntitySettingsAssetsForm.php b/web/modules/webform/src/EntitySettings/WebformEntitySettingsAssetsForm.php
index 40c81de918d5709403c51b099ba2b2f7203254e4..83864a4491dd3372704fbfa75b4c7c329063521d 100644
--- a/web/modules/webform/src/EntitySettings/WebformEntitySettingsAssetsForm.php
+++ b/web/modules/webform/src/EntitySettings/WebformEntitySettingsAssetsForm.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Element\WebformMessage;
 
 /**
  * Webform CSS and JS assets.
@@ -21,6 +22,8 @@ public function form(array $form, FormStateInterface $form_state) {
       '#type' => 'webform_message',
       '#message_message' => $this->t('The below CSS and JavasScript will be loaded on all pages that references and loads this webform.'),
       '#message_type' => 'info',
+      '#message_close' => TRUE,
+      '#message_storage' => WebformMessage::STORAGE_SESSION,
     ];
     $form['css'] = [
       '#type' => 'fieldset',
diff --git a/web/modules/webform/src/EntitySettings/WebformEntitySettingsBaseForm.php b/web/modules/webform/src/EntitySettings/WebformEntitySettingsBaseForm.php
index 7b7b27e11f3beca631c264ee16e2ebcd20215649..f83a740377ee19d00310aa059d0e8f76a1a478e2 100644
--- a/web/modules/webform/src/EntitySettings/WebformEntitySettingsBaseForm.php
+++ b/web/modules/webform/src/EntitySettings/WebformEntitySettingsBaseForm.php
@@ -4,7 +4,8 @@
 
 use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Render\Element;
+use Drupal\webform\Utility\WebformDialogHelper;
+use Drupal\webform\Utility\WebformElementHelper;
 
 /**
  * Base webform entity settings form.
@@ -31,6 +32,13 @@ protected function actions(array $form, FormStateInterface $form_state) {
     if ($this->operation != 'settings') {
       unset($actions['delete']);
     }
+
+    // Open delete button in a modal dialog.
+    if (isset($actions['delete'])) {
+      $actions['delete']['#attributes'] = WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW, $actions['delete']['#attributes']['class']);
+      WebformDialogHelper::attachLibraries($actions['delete']);
+    }
+
     return $actions;
   }
 
@@ -49,7 +57,7 @@ public function save(array $form, FormStateInterface $form_state) {
     ];
     $this->logger('webform')->notice('Webform settings @label has been saved.', $context);
 
-    drupal_set_message($this->t('Webform settings %label has been saved.', ['%label' => $webform->label()]));
+    $this->messenger()->addStatus($this->t('Webform settings %label has been saved.', ['%label' => $webform->label()]));
   }
 
   /**
@@ -62,8 +70,7 @@ public function save(array $form, FormStateInterface $form_state) {
    */
   protected function appendDefaultValueToElementDescriptions(array &$form, array $default_settings) {
     foreach ($form as $key => &$element) {
-      // Skip if not a FAPI element.
-      if (Element::property($key) || !is_array($element)) {
+      if (!WebformElementHelper::isElement($element, $key)) {
         continue;
       }
 
@@ -71,10 +78,11 @@ protected function appendDefaultValueToElementDescriptions(array &$form, array $
         if (!isset($element['#description'])) {
           $element['#description'] = '';
         }
-        $element['#description'] .= ($element['#description'] ? '<br /><br />' : '');
-        // @todo: Stop quotes from being encoded. (i.e. "Submit" => &quot;Submit&quote;)
         $value = $default_settings["default_$key"];
-        $element['#description'] .= $this->t('Defaults to: %value', ['%value' => $value]);
+        if (!is_array($value)) {
+          $element['#description'] .= ($element['#description'] ? '<br /><br />' : '');
+          $element['#description'] .= $this->t('Defaults to: %value', ['%value' => $value]);
+        }
       }
 
       $this->appendDefaultValueToElementDescriptions($element, $default_settings);
diff --git a/web/modules/webform/src/EntitySettings/WebformEntitySettingsConfirmationForm.php b/web/modules/webform/src/EntitySettings/WebformEntitySettingsConfirmationForm.php
index 23af324d939db2e446194449be500049b70921ec..ffd10da6711ca7d4d47e7f7a79dba57352d26262 100644
--- a/web/modules/webform/src/EntitySettings/WebformEntitySettingsConfirmationForm.php
+++ b/web/modules/webform/src/EntitySettings/WebformEntitySettingsConfirmationForm.php
@@ -47,7 +47,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
 
     $elements = $webform->getElementsDecoded();
     if (!empty($elements['#method'])) {
-      drupal_set_message($this->t('Form is being posted using a custom method. Confirmation page must be handled by the <a href=":href">custom form action</a>.', [':href' => $webform->toUrl('settings-form')->toString()]), 'warning');
+      $this->messenger()->addWarning($this->t('Form is being posted using a custom method. Confirmation page must be handled by the <a href=":href">custom form action</a>.', [':href' => $webform->toUrl('settings-form')->toString()]));
       return $form;
     }
 
@@ -71,7 +71,7 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['confirmation_type']['ajax_confirmation'] = [
       '#type' => 'webform_message',
       '#message_type' => 'warning',
-      '#message_message' => $this->t("Only 'Inline', 'Message', and 'Modal' confirmation types are fully supported by Ajax."),
+      '#message_message' => $this->t("Only 'Inline', 'Message', 'Modal', and 'None' confirmation types are fully supported by Ajax."),
       '#access' => $settings['ajax'],
       '#states' => [
         'invisible' => [
@@ -80,6 +80,8 @@ public function form(array $form, FormStateInterface $form_state) {
           [':input[name="confirmation_type"]' => ['value' => WebformInterface::CONFIRMATION_MESSAGE]],
           'or',
           [':input[name="confirmation_type"]' => ['value' => WebformInterface::CONFIRMATION_MODAL]],
+          'or',
+          [':input[name="confirmation_type"]' => ['value' => WebformInterface::CONFIRMATION_NONE]],
         ],
       ],
     ];
@@ -93,6 +95,7 @@ public function form(array $form, FormStateInterface $form_state) {
         WebformInterface::CONFIRMATION_MODAL => $this->t('Modal (reloads the current page/form and displays the confirmation message in a modal dialog)'),
         WebformInterface::CONFIRMATION_URL => $this->t('URL (redirects to a custom path or URL)'),
         WebformInterface::CONFIRMATION_URL_MESSAGE => $this->t('URL with message (redirects to a custom path or URL and displays the confirmation message at the top of the page)'),
+        WebformInterface::CONFIRMATION_NONE => $this->t('None (reloads the current page and does not display a confirmation message)'),
       ],
       '#default_value' => $settings['confirmation_type'],
     ];
@@ -144,6 +147,10 @@ public function form(array $form, FormStateInterface $form_state) {
       '#default_value' => $settings['confirmation_exclude_token'],
       '#access' => !$webform->isResultsDisabled(),
     ];
+    $form['confirmation_url']['token_tree_link'] = $this->tokenManager->buildTreeElement(
+      ['webform', 'webform_submission', 'webform_handler'],
+      $this->t('You may use tokens to pass query string parameters. Make sure all tokens include the urlencode suffix. (i.e. [webform-submission:values:email:urlencode])')
+    );
 
     // Confirmation settings.
     $form['confirmation_settings'] = [
@@ -152,7 +159,9 @@ public function form(array $form, FormStateInterface $form_state) {
       '#open' => TRUE,
       '#states' => [
         'invisible' => [
-          ':input[name="confirmation_type"]' => ['value' => WebformInterface::CONFIRMATION_URL],
+          [':input[name="confirmation_type"]' => ['value' => WebformInterface::CONFIRMATION_URL]],
+          'or',
+          [':input[name="confirmation_type"]' => ['value' => WebformInterface::CONFIRMATION_NONE]],
         ],
       ],
     ];
@@ -176,11 +185,13 @@ public function form(array $form, FormStateInterface $form_state) {
       '#default_value' => $settings['confirmation_message'],
       '#states' => [
         'invisible' => [
-          ':input[name="confirmation_type"]' => ['value' => WebformInterface::CONFIRMATION_URL],
+          [':input[name="confirmation_type"]' => ['value' => WebformInterface::CONFIRMATION_URL]],
+          'or',
+          [':input[name="confirmation_type"]' => ['value' => WebformInterface::CONFIRMATION_NONE]],
         ],
       ],
     ];
-    $form['confirmation_settings']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['confirmation_settings']['token_tree_link'] = $this->tokenManager->buildTreeElement();
 
     // Attributes.
     $form['confirmation_attributes_container'] = [
@@ -244,7 +255,19 @@ public function form(array $form, FormStateInterface $form_state) {
       '#classes' => $this->config('webform.settings')->get('settings.confirmation_back_classes'),
       '#default_value' => $settings['confirmation_back_attributes'],
     ];
-    $form['back']['back_container']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['back']['back_container']['token_tree_link'] = $this->tokenManager->buildTreeElement();
+
+    // None.
+    $form['confirmation_type']['none'] = [
+      '#type' => 'webform_message',
+      '#message_type' => 'warning',
+      '#message_message' => $this->t('This setting assumes that a webform handler will manage the displaying of a confirmation message.'),
+      '#states' => [
+        'visible' => [
+          ':input[name="confirmation_type"]' => ['value' => WebformInterface::CONFIRMATION_NONE],
+        ],
+      ],
+    ];
 
     $this->tokenManager->elementValidate($form);
 
diff --git a/web/modules/webform/src/EntitySettings/WebformEntitySettingsFormForm.php b/web/modules/webform/src/EntitySettings/WebformEntitySettingsFormForm.php
index 1063e3a16f04cc47c6d848f75bb997aad64b9730..9eafdcb7864f3a59f4e4d82ee7994e499b9b2bfa 100644
--- a/web/modules/webform/src/EntitySettings/WebformEntitySettingsFormForm.php
+++ b/web/modules/webform/src/EntitySettings/WebformEntitySettingsFormForm.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\webform\Element\WebformMessage;
 use Drupal\webform\Utility\WebformArrayHelper;
 use Drupal\webform\Utility\WebformDateHelper;
 use Drupal\webform\Utility\WebformElementHelper;
@@ -55,7 +57,7 @@ public function form(array $form, FormStateInterface $form_state) {
     // Form settings.
     $form['form_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Form settings'),
+      '#title' => $this->t('Form general settings'),
       '#open' => TRUE,
     ];
     $form['form_settings']['status'] = [
@@ -80,6 +82,21 @@ public function form(array $form, FormStateInterface $form_state) {
         ],
       ],
     ];
+    $t_args = [
+      ':page_cache_href' => 'https://www.drupal.org/docs/8/administering-a-drupal-8-site/internal-page-cache',
+      ':issue_href' => 'https://www.drupal.org/node/2352009',
+      ':cache_control_override_href' => 'https://www.drupal.org/project/cache_control_override',
+    ];
+    if ($this->moduleHandler->moduleExists('page_cache') && !$this->moduleHandler->moduleExists('cache_control_override')) {
+      $form['form_settings']['scheduled']['page_cache'] = [
+        '#type' => 'webform_message',
+        '#message_type' => 'warning',
+        '#message_close' => TRUE,
+        '#message_storage' => WebformMessage::STORAGE_SESSION,
+        '#message_message' => $this->t('Scheduled forms do not work as expected for anonymous users when Drupal\'s <a href=":page_cache_href">Internal Page Cache</a> module is enabled. This is a <a href=":issue_href">known issue</a>.', $t_args) . '<br/><br/>' .
+          '<strong>' . $this->t('It is strongly recommended that you install the <a href=":cache_control_override_href">Cache Control Override</a> module.', $t_args) . '</strong>',
+      ];
+    }
     $form['form_settings']['scheduled']['open'] = [
       '#type' => 'datetime',
       '#title' => $this->t('Open'),
@@ -90,6 +107,7 @@ public function form(array $form, FormStateInterface $form_state) {
       '#description' => [
         '#type' => 'webform_help',
         '#help' => $this->t('If the open date/time is left blank, this form will immediately be opened.'),
+        '#help_title' => $this->t('Open'),
       ],
     ];
     $form['form_settings']['scheduled']['close'] = [
@@ -102,6 +120,7 @@ public function form(array $form, FormStateInterface $form_state) {
       '#description' => [
         '#type' => 'webform_help',
         '#help' => $this->t('If the close date/time is left blank, this webform will never be closed.'),
+        '#help_title' => $this->t('Close'),
       ],
       '#default_value' => $webform->get('close') ? DrupalDateTime::createFromTimestamp(strtotime($webform->get('close'))) : NULL,
     ];
@@ -110,6 +129,19 @@ public function form(array $form, FormStateInterface $form_state) {
       $form['form_settings']['status']['#access'] = FALSE;
       $form['form_settings']['scheduled']['#access'] = FALSE;
     }
+    $form['form_settings']['form_title'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Form title display'),
+      '#description' => $this->t("Select how the form's title is displayed when this webform is attached to a source entity. This title is only displayed when a webform is linked to from a source entity or opened in dialog."),
+      '#options' => [
+        WebformInterface::TITLE_SOURCE_ENTITY_WEBFORM => $this->t('Source entity: Webform'),
+        WebformInterface::TITLE_WEBFORM_SOURCE_ENTITY => $this->t('Webform: Source entity'),
+        WebformInterface::TITLE_WEBFORM => $this->t('Webform'),
+        WebformInterface::TITLE_SOURCE_ENTITY => $this->t('Source entity'),
+      ],
+      '#required' => TRUE,
+      '#default_value' => $settings['form_title'],
+    ];
     $form['form_settings']['form_open_message'] = [
       '#type' => 'webform_html_editor',
       '#title' => $this->t('Form open message'),
@@ -128,47 +160,20 @@ public function form(array $form, FormStateInterface $form_state) {
       '#description' => $this->t('A message to be displayed if the webform breaks.'),
       '#default_value' => $settings['form_exception_message'],
     ];
-    $form['form_settings']['token_tree_link'] = $this->tokenManager->buildTreeLink();
-
-    // Form attributes.
-    $form['form_attributes'] = [
+    $form['form_settings']['token_tree_link'] = $this->tokenManager->buildTreeElement();
+    $form['form_settings']['form_attributes'] = [
       '#type' => 'details',
       '#title' => $this->t('Form attributes'),
       '#open' => TRUE,
     ];
     $elements = $webform->getElementsDecoded();
-    $form['form_attributes']['attributes'] = [
+    $form['form_settings']['form_attributes']['attributes'] = [
       '#type' => 'webform_element_attributes',
       '#title' => $this->t('Form'),
       '#classes' => $this->config('webform.settings')->get('settings.form_classes'),
       '#default_value' => (isset($elements['#attributes'])) ? $elements['#attributes'] : [],
     ];
 
-    // Form access denied.
-    $form['form_access_denied'] = [
-      '#type' => 'details',
-      '#title' => $this->t('Access denied'),
-      '#open' => TRUE,
-    ];
-    $form['form_access_denied']['form_login'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Redirect to login when access denied to webform'),
-      '#return_value' => TRUE,
-      '#default_value' => $settings['form_login'],
-    ];
-    $form['form_access_denied']['form_login_message'] = [
-      '#type' => 'webform_html_editor',
-      '#title' => $this->t('Login message when access denied to webform'),
-      '#description' => $this->t('A message to be displayed on the login page.'),
-      '#default_value' => $settings['form_login_message'],
-      '#states' => [
-        'visible' => [
-          ':input[name="form_login"]' => ['checked' => TRUE],
-        ],
-      ],
-    ];
-    $form['form_access_denied']['token_tree_link'] = $this->tokenManager->buildTreeLink();
-
     // Form behaviors.
     $form['form_behaviors'] = [
       '#type' => 'details',
@@ -198,10 +203,91 @@ public function form(array $form, FormStateInterface $form_state) {
       ],
     ];
 
+    if ($settings['draft'] !== WebformInterface::DRAFT_NONE) {
+      $form['form_behaviors']['form_reset_message'] = [
+        '#type' => 'webform_message',
+        '#message_type' => 'warning',
+        '#message_message' => $this->t('Currently loaded drafts will be deleted when the form is reset.'),
+        '#weight' => $form['form_behaviors']['form_reset']['#weight'] + 1,
+        '#states' => [
+          'visible' => [
+            ':input[name="form_reset"]' => ['checked' => TRUE],
+          ],
+        ],
+
+      ];
+    }
+
+    // Access denied.
+    $form['access_denied'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Form access denied settings'),
+      '#open' => TRUE,
+    ];
+    $form['access_denied']['form_access_denied'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('When a user is denied access to this webform'),
+      '#description' => $this->t('Select what happens when a user is denied access to this webform.') .
+        '<br/><br/>' .
+        $this->t('Go to <a href=":href">submission settings</a> to select what happens when a user is denied access to submissions.', [':href' => Url::fromRoute('entity.webform.settings_submissions', ['webform' => $webform->id()])->toString()]),
+
+      '#options' => [
+        WebformInterface::ACCESS_DENIED_DEFAULT => $this->t('Default (Displays the default access denied page)'),
+        WebformInterface::ACCESS_DENIED_MESSAGE => $this->t('Inline (Displays message when access is denied to field, nodes, and blocks)'),
+        WebformInterface::ACCESS_DENIED_PAGE => $this->t('Page (Displays message when access is denied to forms, fields, nodes, and blocks)'),
+        WebformInterface::ACCESS_DENIED_LOGIN => $this->t('Login (Redirects to user login form and displays message. Field, nodes, and block only display the message.)'),
+      ],
+      '#required' => TRUE,
+      '#default_value' => $settings['form_access_denied'],
+    ];
+    $form['access_denied']['access_denied_container'] = [
+      '#type' => 'container',
+      '#states' => [
+        'visible' => [
+          ':input[name="form_access_denied"]' => ['!value' => WebformInterface::ACCESS_DENIED_DEFAULT],
+        ],
+      ],
+    ];
+    $form['access_denied']['access_denied_container']['form_access_denied_title'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Access denied title'),
+      '#description' => $this->t('Page title to be shown on access denied page'),
+      '#default_value' => $settings['form_access_denied_title'],
+      '#states' => [
+        'visible' => [
+          ':input[name="form_access_denied"]' => ['value' => WebformInterface::ACCESS_DENIED_PAGE],
+        ],
+      ],
+    ];
+    $form['access_denied']['access_denied_container']['form_access_denied_message'] = [
+      '#type' => 'webform_html_editor',
+      '#title' => $this->t('Access denied message'),
+      '#description' => $this->t('Will be displayed either in-line or as a status message depending on the setting above.'),
+      '#default_value' => $settings['form_access_denied_message'],
+    ];
+    $form['access_denied']['access_denied_container']['token_tree_link'] = $this->tokenManager->buildTreeElement();
+    $form['access_denied']['access_denied_container']['access_denied_attributes'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Access denied message attributes'),
+      '#open' => TRUE,
+      '#states' => [
+        'visible' => [
+          [':input[name="form_access_denied"]' => ['value' => WebformInterface::ACCESS_DENIED_MESSAGE]],
+          'or',
+          [':input[name="form_access_denied"]' => ['value' => WebformInterface::ACCESS_DENIED_PAGE]],
+        ],
+      ],
+    ];
+    $form['access_denied']['access_denied_container']['access_denied_attributes']['form_access_denied_attributes'] = [
+      '#type' => 'webform_element_attributes',
+      '#title' => $this->t('Access denied message'),
+      '#default_value' => $settings['form_access_denied_attributes'],
+    ];
+
     // Wizard settings.
     $form['wizard_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Wizard settings'),
+      '#title' => $this->t('Form wizard settings'),
       '#open' => TRUE,
       '#states' => [
         'visible' => [
@@ -212,36 +298,76 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['wizard_settings']['wizard_progress_bar'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Show wizard progress bar'),
+      '#description' => $this->t('If checked, a progress bar will displayed about the form.'),
       '#return_value' => TRUE,
       '#default_value' => $settings['wizard_progress_bar'],
     ];
+    $form['wizard_settings']['wizard_progress_link'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Link to previous pages in progress bar'),
+      '#description' => $this->t('If checked, previous pages will be link in the progress bar.'),
+      '#return_value' => TRUE,
+      '#default_value' => $settings['wizard_progress_link'],
+      '#states' => [
+        'visible' => [
+          ':input[name="wizard_progress_bar"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
     $form['wizard_settings']['wizard_progress_pages'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Show wizard progress pages'),
+      '#description' => $this->t('If checked, the current page and total remaining pages will be displayed. (i.e. Page 1 of 10)'),
       '#return_value' => TRUE,
       '#default_value' => $settings['wizard_progress_pages'],
     ];
     $form['wizard_settings']['wizard_progress_percentage'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Show wizard progress percentage'),
+      '#description' => $this->t('If checked, the percentage of completed pages will be displayed. (i.e. 10%)'),
       '#return_value' => TRUE,
       '#default_value' => $settings['wizard_progress_percentage'],
     ];
+    $form['wizard_settings']['wizard_preview_link'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Link to previous pages in preview'),
+      '#description' => $this->t("If checked, the preview page will included 'Edit' buttons for each previous page.") . '<br/><br/>' .
+        '<em>' . $this->t("This settings is only available when 'Enable preview page' is enabled.") . '</em>',
+      '#return_value' => TRUE,
+      '#default_value' => $settings['wizard_preview_link'],
+      '#states' => [
+        'enabled' => [
+          ':input[name="preview"]' => ['!value' => DRUPAL_DISABLED],
+        ],
+      ],
+    ];
     $form['wizard_settings']['wizard_confirmation'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Include confirmation page in progress'),
+      '#description' => $this->t("If checked, the confirmation page will be included in the progress bar."),
       '#return_value' => TRUE,
       '#default_value' => $settings['wizard_confirmation'],
+      '#states' => [
+        'visible' => [
+          [':input[name="wizard_progress_bar"]' => ['checked' => TRUE]],
+          'or',
+          [':input[name="wizard_progress_pages"]' => ['checked' => TRUE]],
+          'or',
+          [':input[name="wizard_progress_percentage"]' => ['checked' => TRUE]],
+        ],
+      ],
     ];
     $form['wizard_settings']['wizard_start_label'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Wizard start label'),
+      '#description' => $this->t('The first page label in the progress bar. Subsequent pages are titled by their wizard page title.'),
       '#size' => 20,
       '#default_value' => $settings['wizard_start_label'],
     ];
     $form['wizard_settings']['wizard_confirmation_label'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Wizard end label'),
+      '#description' => $this->t("The confirmation page label's in the progress bar."),
       '#size' => 20,
       '#default_value' => $settings['wizard_confirmation_label'],
       '#states' => [
@@ -253,6 +379,7 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['wizard_settings']['wizard_track'] = [
       '#type' => 'select',
       '#title' => $this->t('Track wizard progress in the URL by'),
+      '#description' => $this->t("Progress tracking allows analytic software to capture a multi-step form's progress."),
       '#options' => [
         'name' => $this->t("Page name (?page=contact)"),
         'index' => $this->t("Page index (?page=2)"),
@@ -264,7 +391,7 @@ public function form(array $form, FormStateInterface $form_state) {
     // Preview settings.
     $form['preview_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Preview settings'),
+      '#title' => $this->t('Form preview settings'),
       '#open' => TRUE,
       '#states' => [
         'visible' => [
@@ -295,7 +422,7 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['preview_settings']['preview_container']['preview_label'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Preview label'),
-      '#description' => $this->t("The text displayed within a multistep wizard's progress bar"),
+      '#description' => $this->t("The text displayed within a multi-step wizard's progress bar"),
       '#default_value' => $settings['preview_label'],
     ];
     $form['preview_settings']['preview_container']['preview_title'] = [
@@ -328,6 +455,12 @@ public function form(array $form, FormStateInterface $form_state) {
       '#return_value' => TRUE,
       '#default_value' => $settings['preview_exclude_empty'],
     ];
+    $form['preview_settings']['preview_container']['elements']['preview_exclude_empty_checkbox'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Exclude unselected checkboxes'),
+      '#return_value' => TRUE,
+      '#default_value' => $settings['preview_exclude_empty_checkbox'],
+    ];
     $form['preview_settings']['preview_container']['preview_attributes'] = [
       '#type' => 'details',
       '#title' => $this->t('Preview attributes'),
@@ -339,7 +472,23 @@ public function form(array $form, FormStateInterface $form_state) {
       '#classes' => $this->config('webform.settings')->get('settings.preview_classes'),
       '#default_value' => $settings['preview_attributes'],
     ];
-    $form['preview_settings']['preview_container']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['preview_settings']['preview_container']['token_tree_link'] = $this->tokenManager->buildTreeElement();
+
+    // File settings.
+    $form['file_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('File settings'),
+      '#open' => TRUE,
+      '#access' => $webform->hasManagedFile(),
+    ];
+    $form['file_settings']['form_file_limit'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('File upload limit'),
+      '#description' => $this->t('Enter a value like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes) in order to set the file upload limit for this form.'),
+      '#element_validate' => [['\Drupal\webform\Form\AdminConfig\WebformAdminConfigElementsForm', 'validateMaxFilesize']],
+      '#size' => 10,
+      '#default_value' => $settings['form_file_limit'],
+    ];
 
     // Custom settings.
     $properties = WebformElementHelper::getProperties($webform->getElementsDecoded());
@@ -350,13 +499,13 @@ public function form(array $form, FormStateInterface $form_state) {
     ];
     $form['custom_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Custom settings'),
+      '#title' => $this->t('Form custom settings'),
       '#open' => array_filter($properties) ? TRUE : FALSE,
       '#access' => !$this->moduleHandler->moduleExists('webform_ui') || $this->currentUser()->hasPermission('edit webform source'),
     ];
     $form['custom_settings']['method'] = [
       '#type' => 'select',
-      '#title' => $this->t('Method'),
+      '#title' => $this->t('Form method'),
       '#description' => $this->t('The HTTP method with which the form will be submitted.') . '<br /><br />' .
         '<em>' . $this->t('Selecting a custom POST or GET method will automatically disable wizards, previews, drafts, submissions, limits, purging, confirmations, emails, and handlers.') . '</em>',
       '#options' => [
@@ -379,7 +528,7 @@ public function form(array $form, FormStateInterface $form_state) {
 
     $form['custom_settings']['action'] = [
       '#type' => 'textfield',
-      '#title' => $this->t('Action'),
+      '#title' => $this->t('Form action'),
       '#description' => $this->t('The URL or path to which the webform will be submitted.'),
       '#states' => [
         'invisible' => [
@@ -401,7 +550,7 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['custom_settings']['custom'] = [
       '#type' => 'webform_codemirror',
       '#mode' => 'yaml',
-      '#title' => $this->t('Custom properties'),
+      '#title' => $this->t('Form custom properties'),
       '#description' =>
         $this->t('Properties do not have to prepended with a hash (#) character, the hash character will be automatically added to the custom properties.') .
         '<br /><br />' .
@@ -515,7 +664,7 @@ protected function getFormBehaviors() {
       'form_reset' => [
         'group' => $this->t('Form'),
         'title' => $this->t('Display reset button'),
-        'form_description' => $this->t("If checked, users will be able to reset a form and restart multistep wizards."),
+        'form_description' => $this->t("If checked, users will be able to reset a form and restart multi-step wizards. Current drafts will be deleted when the form is reset."),
       ],
       'form_submit_once' => [
         'group' => $this->t('Form'),
@@ -556,7 +705,6 @@ protected function getFormBehaviors() {
         'title' => $this->t('Disable inline form errors'),
         'all_description' => $this->t('Inline form errors is disabled for all forms.'),
         'form_description' => $this->t('If checked, <a href=":href">inline form errors</a> will be disabled for this form.', [':href' => 'https://www.drupal.org/docs/8/core/modules/inline-form-errors/inline-form-errors-module-overview']),
-        'access' => (\Drupal::moduleHandler()->moduleExists('inline_form_errors') && floatval(\Drupal::VERSION) >= 8.5),
       ],
       'form_required' => [
         'group' => $this->t('Validation'),
diff --git a/web/modules/webform/src/EntitySettings/WebformEntitySettingsGeneralForm.php b/web/modules/webform/src/EntitySettings/WebformEntitySettingsGeneralForm.php
index 4890d37f00c1bb0297e1663eeea2fa67c7828add..9da0bb18f08cad4fe61d31e0ae8d16d8dbabe2cc 100644
--- a/web/modules/webform/src/EntitySettings/WebformEntitySettingsGeneralForm.php
+++ b/web/modules/webform/src/EntitySettings/WebformEntitySettingsGeneralForm.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\webform\EntitySettings;
 
+use Drupal\Component\Serialization\Json;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Url;
@@ -107,6 +108,13 @@ public function form(array $form, FormStateInterface $form_state) {
       '#access' => $this->moduleHandler->moduleExists('webform_templates'),
       '#default_value' => $webform->isTemplate(),
     ];
+    $form['general_settings']['archive'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Archive this webform'),
+      '#description' => $this->t('If checked, this webform will be closed and unavailable to webform blocks and fields.'),
+      '#return_value' => TRUE,
+      '#default_value' => $webform->isArchived(),
+    ];
     $form['general_settings']['results_disabled'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Disable saving of submissions'),
@@ -242,11 +250,6 @@ public function form(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Ajax settings'),
       '#open' => TRUE,
       '#access' => empty($elements['#method']),
-      '#states' => [
-        'visible' => [
-          ':input[name="method"]' => ['value' => ''],
-        ],
-      ],
     ];
     $form['ajax_settings']['ajax'] = [
       '#type' => 'checkbox',
@@ -257,7 +260,7 @@ public function form(array $form, FormStateInterface $form_state) {
     ];
     $form['ajax_settings']['ajax_scroll_top'] = [
       '#type' => 'radios',
-      '#title' => $this->t('On Ajax load, scroll to the top of the...'),
+      '#title' => $this->t('On Ajax load, scroll to the top of the…'),
       '#description' => $this->t("Select where the page should be scrolled to when paging, saving of drafts, previews, submissions, and confirmations. Select 'None' to disable scrolling."),
       '#options' => [
         '' => $this->t('None'),
@@ -272,6 +275,79 @@ public function form(array $form, FormStateInterface $form_state) {
       '#default_value' => $settings['ajax_scroll_top'],
     ];
 
+    // Dialog settings.
+    if ($default_settings['dialog']) {
+      $rows = [];
+      // Preset examples.
+      foreach ($default_settings['dialog_options'] as $dialog_name => $dialog_options) {
+        $dialog_options += [
+          'width' => $this->t('auto'),
+          'height' => $this->t('auto'),
+        ];
+        $dialog_link = [
+          '#type' => 'link',
+          '#url' => $webform->toUrl(),
+          '#title' => $this->t('Test @title', ['@title' => $dialog_options['title']]),
+          '#attributes' => [
+            'class' => ['webform-dialog', 'webform-dialog-' . $dialog_name, 'button'],
+          ],
+        ];
+        $row = [];
+        $row['title'] = $dialog_options['title'];
+        $row['dimensions'] = $dialog_options['width'] . ' x ' . $dialog_options['height'];
+        $row['link'] = ['data' => $dialog_link, 'nowrap' => 'nowrap'];
+        $row['source'] = $this->buildDialogSource($dialog_link);
+        $rows[$dialog_name] = $row;
+      }
+
+      // Custom example.
+      $dialog_link = [
+        '#type' => 'link',
+        '#title' => $this->t('Test Custom'),
+        '#url' => $webform->toUrl(),
+        '#attributes' => [
+          'class' => ['webform-dialog', 'button'],
+          'data-dialog-options' => Json::encode([
+            'width' => 400,
+            'height' => 400,
+          ]),
+        ],
+      ];
+      $row = [];
+      $row['title'] = $this->t('Custom');
+      $row['dimensions'] = '400 x 400';
+      $row['link'] = ['data' => $dialog_link];
+      $row['source'] = $this->buildDialogSource($dialog_link);
+      $rows['custom'] = $row;
+
+      $form['dialog_settings'] = [
+        '#type' => 'details',
+        '#title' => $this->t('Dialog settings'),
+        '#description' => $this->t('Below are links and code snippets that can be inserted into your website to open this form in a modal dialog.'),
+        '#open' => TRUE,
+        'table' => [
+          '#type' => 'table',
+          '#header' => [
+            ['data' => $this->t('Title'), 'width' => '10%', 'class' => [RESPONSIVE_PRIORITY_LOW]],
+            ['data' => $this->t('Dimensions'), 'width' => '10%', 'class' => [RESPONSIVE_PRIORITY_LOW]],
+            ['data' => $this->t('Example'), 'width' => '10%', 'class' => [RESPONSIVE_PRIORITY_LOW]],
+            ['data' => $this->t('Source'), 'width' => '70%'],
+          ],
+          '#rows' => $rows,
+        ],
+      ];
+
+      $form['dialog_settings']['form_prepopulate_source_entity'] = [
+        '#type' => 'checkbox',
+        '#title' => $this->t('Allow (dialog) source entity to be populated using query string parameters'),
+        '#description' => $this->t("If checked, source entity can be populated using query string parameters.") .
+          '<br/><br/>' . $this->t("For example, appending <code>?source_entity_type=node&source_entity_id=1</code> to a webform's URL would set a submission's 'Submitted to' value to 'node:1'.") .
+          '<br/><br/>' . $this->t("You can also append <code>?source_entity_type=ENTITY_TYPE&amp;source_entity_id=ENTITY_ID</code> and the <code>ENTITY_TYPE</code> and <code>ENTITY_ID</code> parameters will automatically be replaced based on the current page's source entity."),
+        '#return_value' => TRUE,
+        '#default_value' => $settings['form_prepopulate_source_entity'],
+      ];
+    }
+
     if ($this->currentUser()->hasPermission('administer webform')) {
       // Author information.
       $form['author_information'] = [
@@ -293,6 +369,21 @@ public function form(array $form, FormStateInterface $form_state) {
       ];
     }
 
+    // Advanced settings.
+    $form['advanced_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Advanced settings'),
+      '#open' => TRUE,
+      '#access' => $this->moduleHandler->moduleExists('webform_node'),
+    ];
+    $form['advanced_settings']['weight'] = [
+      '#type' => 'weight',
+      '#title' => $this->t('Weight'),
+      '#description' => $this->t('Weight is used when multiple webforms are associated to the same webform node.'),
+      '#default_value' => $webform->get('weight'),
+      '#access' => $this->moduleHandler->moduleExists('webform_node'),
+    ];
+
     // Third party settings.
     $form['third_party_settings'] = [
       '#type' => 'details',
@@ -308,6 +399,8 @@ public function form(array $form, FormStateInterface $form_state) {
       ksort($form['third_party_settings']);
     }
 
+    $form['#attached']['library'][] = 'webform/webform.admin.settings';
+
     return parent::form($form, $form_state);
   }
 
@@ -338,6 +431,7 @@ public function save(array $form, FormStateInterface $form_state) {
       $values['title'],
       $values['description'],
       $values['category'],
+      $values['weight'],
       $values['template'],
       $values['uid']
     );
@@ -348,4 +442,46 @@ public function save(array $form, FormStateInterface $form_state) {
     parent::save($form, $form_state);
   }
 
+  /**
+   * Build dialog source.
+   *
+   * @param array $link
+   *   Webform link.
+   *
+   * @return array
+   *   A renderable array containing dialog source
+   */
+  protected function buildDialogSource(array $link) {
+    $source_entity_link = $link;
+    $source_entity_link['#url'] = clone $source_entity_link['#url'];
+    $source_entity_link['#url']->setOption('query', ['source_entity_type' => 'ENTITY_TYPE', 'source_entity_id' => 'ENTITY_ID']);
+
+    return [
+      'data' => [
+        'webform' => [
+          '#theme' => 'webform_codemirror',
+          '#type' => 'html',
+          '#code' => (string) \Drupal::service('renderer')->renderPlain($link),
+          '#suffix' => '<br/>',
+        ],
+        'source_entity' => [
+          'container' => [
+            '#type' => 'container',
+            '#attributes' => ['class' => ['js-form-item']],
+            '#states' => [
+              'visible' => [
+                ':input[name="form_prepopulate_source_entity"]' => ['checked' => TRUE],
+              ],
+            ],
+            'link' => [
+              '#theme' => 'webform_codemirror',
+              '#type' => 'html',
+              '#code' => (string) \Drupal::service('renderer')->renderPlain($source_entity_link),
+            ],
+          ],
+        ],
+      ],
+    ];
+  }
+
 }
diff --git a/web/modules/webform/src/EntitySettings/WebformEntitySettingsSubmissionsForm.php b/web/modules/webform/src/EntitySettings/WebformEntitySettingsSubmissionsForm.php
index bb5f3797e60e39e296c98d66a1b7bf92008af414..34abd39c9f6f0804dc6abab975b3c5678895042a 100644
--- a/web/modules/webform/src/EntitySettings/WebformEntitySettingsSubmissionsForm.php
+++ b/web/modules/webform/src/EntitySettings/WebformEntitySettingsSubmissionsForm.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
+use Drupal\views\Entity\View;
+use Drupal\webform\Element\WebformMessage;
 use Drupal\webform\Utility\WebformDateHelper;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformSubmissionStorageInterface;
@@ -50,7 +52,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
 
     // Display warning and disable the submission form.
     if ($webform->isResultsDisabled()) {
-      drupal_set_message($this->t('Saving of submissions is disabled, submission settings, submission limits, purging and the saving of drafts is disabled. Submissions must be sent via an email or handled using a <a href=":href">custom webform handler</a>.', [':href' => $webform->toUrl('handlers')->toString()]), 'warning');
+      $this->messenger()->addWarning($this->t('Saving of submissions is disabled, submission settings, submission limits, purging and the saving of drafts is disabled. Submissions must be sent via an email or handled using a <a href=":href">custom webform handler</a>.', [':href' => $webform->toUrl('handlers')->toString()]));
       return $form;
     }
 
@@ -76,7 +78,7 @@ public function form(array $form, FormStateInterface $form_state) {
     // Submission settings.
     $form['submission_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Submission settings'),
+      '#title' => $this->t('Submission general settings'),
       '#open' => TRUE,
     ];
     $form['submission_settings']['submission_label'] = [
@@ -93,9 +95,21 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['submission_settings']['submission_locked_message'] = [
       '#type' => 'webform_html_editor',
       '#title' => $this->t('Submission locked message'),
-      '#description' => $this->t('A message to be displayed if submission is lockec.'),
+      '#description' => $this->t('A message to be displayed if submission is locked.'),
       '#default_value' => $settings['submission_locked_message'],
     ];
+    $form['submission_settings']['previous_submission_message'] = [
+      '#type' => 'webform_html_editor',
+      '#title' => $this->t('Previous submission message'),
+      '#description' => $this->t('A message to be displayed when there is previous submission.'),
+      '#default_value' => $settings['previous_submission_message'],
+    ];
+    $form['submission_settings']['previous_submissions_message'] = [
+      '#type' => 'webform_html_editor',
+      '#title' => $this->t('Previous submissions message'),
+      '#description' => $this->t('A message to be displayed when there are previous submissions.'),
+      '#default_value' => $settings['previous_submissions_message'],
+    ];
     $form['submission_settings']['next_serial'] = [
       '#type' => 'number',
       '#title' => $this->t('Next submission number'),
@@ -103,68 +117,30 @@ public function form(array $form, FormStateInterface $form_state) {
       '#min' => 1,
       '#default_value' => $webform_storage->getNextSerial($webform),
     ];
-    $form['submission_settings']['token_tree_link'] = $this->tokenManager->buildTreeLink();
-    $form['submission_settings']['submission_columns'] = [
+    $form['submission_settings']['token_tree_link'] = $this->tokenManager->buildTreeElement();
+    $form['submission_settings']['submission_container']['elements'] = [
       '#type' => 'details',
-      '#title' => $this->t('Submission columns'),
-      '#description' => $this->t('Below columns are displayed to users who can view previous submissions and/or pending drafts.'),
-    ];
-    // Submission user columns.
-    // @see \Drupal\webform\Form\WebformResultsCustomForm::buildForm
-    $available_columns = $webform_submission_storage->getColumns($webform);
-    // Remove columns that should never be displayed to users.
-    $available_columns = array_diff_key($available_columns, array_flip(['uuid', 'in_draft', 'entity', 'sticky', 'locked', 'notes', 'uid', 'operations']));
-    $custom_columns = $webform_submission_storage->getUserColumns($webform);
-    // Change sid's # to an actual label.
-    $available_columns['sid']['title'] = $this->t('Submission ID');
-    if (isset($custom_columns['sid'])) {
-      $custom_columns['sid']['title'] = $this->t('Submission ID');
-    }
-    // Get available columns as option.
-    $columns_options = [];
-    foreach ($available_columns as $column_name => $column) {
-      $title = (strpos($column_name, 'element__') === 0) ? ['data' => ['#markup' => '<b>' . $column['title'] . '</b>']] : $column['title'];
-      $key = (isset($column['key'])) ? str_replace('webform_', '', $column['key']) : $column['name'];
-      $columns_options[$column_name] = ['title' => $title, 'key' => $key];
-    }
-    // Get custom columns as the default value.
-    $columns_keys = array_keys($custom_columns);
-    $columns_default_value = array_combine($columns_keys, $columns_keys);
-    // Display columns in sortable table select element.
-    $form['submission_settings']['submission_columns']['submission_user_columns'] = [
-      '#type' => 'webform_tableselect_sort',
-      '#header' => [
-        'title' => $this->t('Title'),
-        'key' => $this->t('Key'),
-      ],
-      '#options' => $columns_options,
-      '#default_value' => $columns_default_value,
+      '#title' => $this->t('Included submission values'),
+      '#description' => $this->t('If you wish to include only parts of the submission when viewing as HTML, table, or plain text, select the elements that should be included. Please note, element specific access controls are still applied to displayed elements.'),
+      '#open' => $settings['submission_excluded_elements'] ? TRUE : FALSE,
     ];
-
-    // Submission access denied.
-    $form['submission_access_denied'] = [
-      '#type' => 'details',
-      '#title' => $this->t('Access denied'),
-      '#open' => TRUE,
+    $form['submission_settings']['submission_container']['elements']['submission_excluded_elements'] = [
+      '#type' => 'webform_excluded_elements',
+      '#webform_id' => $this->getEntity()->id(),
+      '#default_value' => $settings['submission_excluded_elements'],
     ];
-    $form['submission_access_denied']['submission_login'] = [
+    $form['submission_settings']['submission_container']['elements']['submission_exclude_empty'] = [
       '#type' => 'checkbox',
-      '#title' => $this->t('Redirect to login when access denied to submission'),
+      '#title' => $this->t('Exclude empty elements'),
       '#return_value' => TRUE,
-      '#default_value' => $settings['submission_login'],
+      '#default_value' => $settings['submission_exclude_empty'],
     ];
-    $form['submission_access_denied']['submission_login_message'] = [
-      '#type' => 'webform_html_editor',
-      '#title' => $this->t('Login message when access denied to submission'),
-      '#description' => $this->t('A message to be displayed on the login page.'),
-      '#default_value' => $settings['submission_login_message'],
-      '#states' => [
-        'visible' => [
-          ':input[name="submission_login"]' => ['checked' => TRUE],
-        ],
-      ],
+    $form['submission_settings']['submission_container']['elements']['submission_exclude_empty_checkbox'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Exclude unselected checkboxes'),
+      '#return_value' => TRUE,
+      '#default_value' => $settings['submission_exclude_empty_checkbox'],
     ];
-    $form['submission_access_denied']['token_tree_link'] = $this->tokenManager->buildTreeLink();
 
     // Submission behaviors.
     $form['submission_behaviors'] = [
@@ -192,6 +168,19 @@ public function form(array $form, FormStateInterface $form_state) {
       ],
       '#weight' => -99,
     ];
+    $form['submission_behaviors']['form_remote_addr'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Track user IP address'),
+      '#description' => $this->t("If checked, a user's IP address will be recorded."),
+      '#return_value' => TRUE,
+      '#default_value' => $settings['form_remote_addr'],
+      '#states' => [
+        'visible' => [
+          ':input[name="form_confidential"]' => ['checked' => FALSE],
+        ],
+      ],
+      '#weight' => -98,
+    ];
     $form['submission_behaviors']['form_convert_anonymous'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Convert anonymous user drafts and submissions to authenticated user'),
@@ -203,7 +192,7 @@ public function form(array $form, FormStateInterface $form_state) {
           ':input[name="form_confidential"]' => ['checked' => FALSE],
         ],
       ],
-      '#weight' => -98,
+      '#weight' => -97,
     ];
     $behavior_elements = [
       // Form specific behaviors.
@@ -212,7 +201,7 @@ public function form(array $form, FormStateInterface $form_state) {
         'form_description' => $this->t('Show the previous submissions notification that appears when users have previously submitted this form.'),
       ],
       'token_update' => [
-        'title' => $this->t('Allow users to update a submission using a secure token.'),
+        'title' => $this->t('Allow users to update a submission using a secure token'),
         'form_description' => $this->t("If checked users will be able to update a submission using the webform's URL appended with the submission's (secure) token.") . ' ' .
           $this->t("The 'tokenized' URL to update a submission will be available when viewing a submission's information and can be inserted into an email using the [webform_submission:update-url] token.") . ' ' .
           $this->t('Only webforms that are open to new submissions can be updated using the secure token.'),
@@ -238,23 +227,144 @@ public function form(array $form, FormStateInterface $form_state) {
       '#weight' => $form['submission_behaviors']['token_update']['#weight'] + 1,
     ];
 
+    // User settings.
+    $form['submission_user_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Submission user settings'),
+      '#open' => TRUE,
+    ];
+    $form['submission_user_settings']['submission_user_duplicate'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Allow users to duplicate previous submissions'),
+      '#description' => $this->t('If checked, users will be able to duplicate their previous submissions.'),
+      '#return_value' => TRUE,
+      '#default_value' => $settings['submission_user_duplicate'],
+    ];
+    $form['submission_user_settings']['submission_columns'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Submission user columns'),
+      '#description' => $this->t('Below columns are displayed to users who can view previous submissions and/or pending drafts.'),
+    ];
+    // Submission user columns.
+    // @see \Drupal\webform\Form\WebformResultsCustomForm::buildForm
+    $available_columns = $webform_submission_storage->getColumns($webform);
+    // Remove columns that should never be displayed to users.
+    $available_columns = array_diff_key($available_columns, array_flip(['uuid', 'in_draft', 'entity', 'sticky', 'locked', 'notes', 'uid']));
+    $custom_columns = $webform_submission_storage->getUserColumns($webform);
+    // Change sid's # to an actual label.
+    $available_columns['sid']['title'] = $this->t('Submission ID');
+    if (isset($custom_columns['sid'])) {
+      $custom_columns['sid']['title'] = $this->t('Submission ID');
+    }
+    // Get available columns as option.
+    $columns_options = [];
+    foreach ($available_columns as $column_name => $column) {
+      $title = (strpos($column_name, 'element__') === 0) ? ['data' => ['#markup' => '<b>' . $column['title'] . '</b>']] : $column['title'];
+      $key = (isset($column['key'])) ? str_replace('webform_', '', $column['key']) : $column['name'];
+      $columns_options[$column_name] = ['title' => $title, 'key' => $key];
+    }
+    // Get custom columns as the default value.
+    $columns_keys = array_keys($custom_columns);
+    $columns_default_value = array_combine($columns_keys, $columns_keys);
+    // Display columns in sortable table select element.
+    $form['submission_user_settings']['submission_columns']['submission_user_columns'] = [
+      '#type' => 'webform_tableselect_sort',
+      '#header' => [
+        'title' => $this->t('Title'),
+        'key' => $this->t('Key'),
+      ],
+      '#options' => $columns_options,
+      '#default_value' => $columns_default_value,
+    ];
+
+    // Access denied.
+    $form['access_denied'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Submission access denied settings'),
+      '#open' => TRUE,
+    ];
+    $form['access_denied']['submission_access_denied'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('When a user is denied access to a submission'),
+      '#description' => $this->t('Select what happens when a user is denied access to a submission.') .
+        '<br/><br/>' .
+        $this->t('Go to <a href=":href">form settings</a> to select what happens when a user is denied access to a webform.', [':href' => Url::fromRoute('entity.webform.settings_form', ['webform' => $webform->id()])->toString()]),
+      '#options' => [
+        WebformInterface::ACCESS_DENIED_DEFAULT => $this->t('Default (Displays the default access denied page)'),
+        WebformInterface::ACCESS_DENIED_PAGE => $this->t('Page (Displays message when access is denied to a submission)'),
+        WebformInterface::ACCESS_DENIED_LOGIN => $this->t('Login (Redirects to user login form and displays message)'),
+      ],
+      '#required' => TRUE,
+      '#default_value' => $settings['submission_access_denied'],
+    ];
+    $form['access_denied']['access_denied_container'] = [
+      '#type' => 'container',
+      '#states' => [
+        'visible' => [
+          ':input[name="submission_access_denied"]' => ['!value' => WebformInterface::ACCESS_DENIED_DEFAULT],
+        ],
+      ],
+    ];
+    $form['access_denied']['access_denied_container']['submission_access_denied_title'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Access denied title'),
+      '#description' => $this->t('Page title to be shown on access denied page'),
+      '#default_value' => $settings['submission_access_denied_title'],
+      '#states' => [
+        'visible' => [
+          ':input[name="submission_access_denied"]' => ['value' => WebformInterface::ACCESS_DENIED_PAGE],
+        ],
+      ],
+    ];
+    $form['access_denied']['access_denied_container']['submission_access_denied_message'] = [
+      '#type' => 'webform_html_editor',
+      '#title' => $this->t('Access denied message'),
+      '#description' => $this->t('Will be displayed either in-line or as a status message depending on the setting above.'),
+      '#default_value' => $settings['submission_access_denied_message'],
+    ];
+    $form['access_denied']['access_denied_container']['token_tree_link'] = $this->tokenManager->buildTreeElement();
+    $form['access_denied']['access_denied_container']['access_denied_attributes'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Access denied message attributes'),
+      '#open' => TRUE,
+      '#states' => [
+        'visible' => [
+          ':input[name="submission_access_denied"]' => ['value' => WebformInterface::ACCESS_DENIED_PAGE],
+        ],
+      ],
+    ];
+    $form['access_denied']['access_denied_container']['access_denied_attributes']['submission_access_denied_attributes'] = [
+      '#type' => 'webform_element_attributes',
+      '#title' => $this->t('Access denied message'),
+      '#default_value' => $settings['submission_access_denied_attributes'],
+    ];
+
     // Submission limits.
     $form['submission_limits'] = [
       '#type' => 'details',
-      '#title' => $this->t('Submission limits'),
+      '#title' => $this->t('Submission limit settings'),
       '#open' => TRUE,
     ];
+    // Submission limits: Total.
     $form['submission_limits']['total'] = [
       '#type' => 'details',
       '#title' => $this->t('Total submissions'),
     ];
-    $form['submission_limits']['total']['limit_total'] = [
+    $form['submission_limits']['total']['total_container'] = [
+      '#type' => 'container',
+      '#states' => [
+        'visible' => [
+          ':input[name="limit_total_unique"]' => ['checked' => FALSE],
+        ],
+      ],
+    ];
+    $form['submission_limits']['total']['total_container']['limit_total'] = [
       '#type' => 'number',
       '#title' => $this->t('Total submissions limit'),
       '#min' => 1,
       '#default_value' => $settings['limit_total'],
     ];
-    $form['submission_limits']['total']['limit_total_interval'] = [
+    $form['submission_limits']['total']['total_container']['limit_total_interval'] = [
       '#type' => 'select',
       '#options' => WebformDateHelper::getIntervalOptions(),
       '#title' => $this->t('Total submissions limit interval'),
@@ -263,13 +373,13 @@ public function form(array $form, FormStateInterface $form_state) {
         'visible' => [':input[name="limit_total"]' => ['!value' => '']],
       ],
     ];
-    $form['submission_limits']['total']['entity_limit_total'] = [
+    $form['submission_limits']['total']['total_container']['entity_limit_total'] = [
       '#type' => 'number',
       '#title' => $this->t('Total submissions limit per source entity'),
       '#min' => 1,
       '#default_value' => $settings['entity_limit_total'],
     ];
-    $form['submission_limits']['total']['entity_limit_total_interval'] = [
+    $form['submission_limits']['total']['total_container']['entity_limit_total_interval'] = [
       '#type' => 'select',
       '#options' => WebformDateHelper::getIntervalOptions(),
       '#title' => $this->t('Total submissions limit interval per source entity'),
@@ -278,7 +388,7 @@ public function form(array $form, FormStateInterface $form_state) {
         'visible' => [':input[name="entity_limit_total"]' => ['!value' => '']],
       ],
     ];
-    $form['submission_limits']['total']['limit_total_message'] = [
+    $form['submission_limits']['total']['total_container']['limit_total_message'] = [
       '#type' => 'webform_html_editor',
       '#title' => $this->t('Total submissions limit message'),
       '#min' => 1,
@@ -291,18 +401,74 @@ public function form(array $form, FormStateInterface $form_state) {
         ],
       ],
     ];
+    $form['submission_limits']['total']['total_container']['token_tree_link'] = $this->tokenManager->buildTreeElement();
+    if ($form['submission_limits']['total']['total_container']['token_tree_link']) {
+      $form['submission_limits']['total']['total_container']['token_tree_link'] += [
+        '#states' => [
+          'visible' => [
+            [':input[name="limit_total"]' => ['!value' => '']],
+            'or',
+            [':input[name="entity_limit_total"]' => ['!value' => '']],
+          ],
+        ],
+      ];
+    }
+    $form['submission_limits']['total']['limit_total_unique'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Limit total to one submission per webform/source entity'),
+      '#default_value' => $settings['limit_total_unique'],
+    ];
+    $form['submission_limits']['total']['limit_total_unique_info'] = [
+      '#type' => 'webform_message',
+      '#message_message' => $this->t('Only submission administrators will only be able to create and update the unique submission.') . '<br/><br/>' .
+        $this->t('Webform blocks can be used to place this webform on the desired source entity types.'),
+      '#message_type' => 'info',
+      '#message_close' => TRUE,
+      '#message_storage' => WebformMessage::STORAGE_SESSION,
+      '#states' => [
+        'visible' => [
+          ':input[name="limit_total_unique"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    $form['submission_limits']['total']['limit_total_unique_warning'] = [
+      '#type' => 'webform_message',
+      '#message_message' => $this->t("Please make sure users are allowed to 'edit any submission'."),
+      '#message_type' => 'warning',
+      '#message_close' => TRUE,
+      '#message_storage' => WebformMessage::STORAGE_SESSION,
+      '#states' => [
+        'visible' => [
+          ':input[name="limit_total_unique"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    // Submission limits: User.
     $form['submission_limits']['user'] = [
       '#type' => 'details',
       '#title' => $this->t('Per user'),
       '#description' => $this->t('Limit the number of submissions per user. A user is identified by their user id if logged-in, or by their Cookie if anonymous.'),
+      '#states' => [
+        'visible' => [
+          ':input[name="limit_total_unique"]' => ['checked' => FALSE],
+        ],
+      ],
+    ];
+    $form['submission_limits']['user']['user_container'] = [
+      '#type' => 'container',
+      '#states' => [
+        'visible' => [
+          ':input[name="limit_user_unique"]' => ['checked' => FALSE],
+        ],
+      ],
     ];
-    $form['submission_limits']['user']['limit_user'] = [
+    $form['submission_limits']['user']['user_container']['limit_user'] = [
       '#type' => 'number',
       '#title' => $this->t('Per user submission limit'),
       '#min' => 1,
       '#default_value' => $settings['limit_user'],
     ];
-    $form['submission_limits']['user']['limit_user_interval'] = [
+    $form['submission_limits']['user']['user_container']['limit_user_interval'] = [
       '#type' => 'select',
       '#options' => WebformDateHelper::getIntervalOptions(),
       '#title' => $this->t('Per user submission limit interval'),
@@ -311,13 +477,13 @@ public function form(array $form, FormStateInterface $form_state) {
         'visible' => [':input[name="limit_user"]' => ['!value' => '']],
       ],
     ];
-    $form['submission_limits']['user']['entity_limit_user'] = [
+    $form['submission_limits']['user']['user_container']['entity_limit_user'] = [
       '#type' => 'number',
       '#min' => 1,
       '#title' => $this->t('Per user submission limit per source entity'),
       '#default_value' => $settings['entity_limit_user'],
     ];
-    $form['submission_limits']['user']['entity_limit_user_interval'] = [
+    $form['submission_limits']['user']['user_container']['entity_limit_user_interval'] = [
       '#type' => 'select',
       '#options' => WebformDateHelper::getIntervalOptions(),
       '#title' => $this->t('Per user submission limit interval per source entity'),
@@ -326,7 +492,7 @@ public function form(array $form, FormStateInterface $form_state) {
         'visible' => [':input[name="entity_limit_user"]' => ['!value' => '']],
       ],
     ];
-    $form['submission_limits']['user']['limit_user_message'] = [
+    $form['submission_limits']['user']['user_container']['limit_user_message'] = [
       '#type' => 'webform_html_editor',
       '#title' => $this->t('Per user submission limit message'),
       '#default_value' => $settings['limit_user_message'],
@@ -338,11 +504,52 @@ public function form(array $form, FormStateInterface $form_state) {
         ],
       ],
     ];
+    $form['submission_limits']['user']['user_container']['token_tree_link'] = $this->tokenManager->buildTreeElement();
+    if ($form['submission_limits']['user']['user_container']['token_tree_link']) {
+      $form['submission_limits']['user']['user_container']['token_tree_link'] += [
+        '#states' => [
+          'visible' => [
+            [':input[name="limit_user"]' => ['!value' => '']],
+            'or',
+            [':input[name="entity_limit_user"]' => ['!value' => '']],
+          ],
+        ],
+      ];
+    }
+    $form['submission_limits']['user']['limit_user_unique'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Limit users to one submission per webform/source entity'),
+      '#default_value' => $settings['limit_user_unique'],
+    ];
+    $form['submission_limits']['user']['limit_user_unique_info'] = [
+      '#type' => 'webform_message',
+      '#message_message' => $this->t('Only authenticated users will be able to create and update their unique submission.'),
+      '#message_type' => 'info',
+      '#message_close' => TRUE,
+      '#message_storage' => WebformMessage::STORAGE_SESSION,
+      '#states' => [
+        'visible' => [
+          ':input[name="limit_user_unique"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    $form['submission_limits']['user']['limit_user_unique_warning'] = [
+      '#type' => 'webform_message',
+      '#message_message' => $this->t("Please make sure users are allowed to 'edit own submission'."),
+      '#message_type' => 'warning',
+      '#message_close' => TRUE,
+      '#message_storage' => WebformMessage::STORAGE_SESSION,
+      '#states' => [
+        'visible' => [
+          ':input[name="limit_user_unique"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
 
     // Purge settings.
     $form['purge_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Submission purging'),
+      '#title' => $this->t('Submission purge settings'),
       '#open' => TRUE,
     ];
     $form['purge_settings']['purge'] = [
@@ -371,7 +578,7 @@ public function form(array $form, FormStateInterface $form_state) {
     // Draft settings.
     $form['draft_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Draft settings'),
+      '#title' => $this->t('Submission draft settings'),
       '#open' => TRUE,
     ];
     $form['draft_settings']['draft'] = [
@@ -432,12 +639,12 @@ public function form(array $form, FormStateInterface $form_state) {
       '#description' => $this->t('Message to be displayed when a draft is loaded.'),
       '#default_value' => $settings['draft_loaded_message'],
     ];
-    $form['draft_settings']['draft_container']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['draft_settings']['draft_container']['token_tree_link'] = $this->tokenManager->buildTreeElement();
 
     // Autofill settings.
     $form['autofill_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Autofill settings'),
+      '#title' => $this->t('Submission autofill settings'),
       '#open' => TRUE,
     ];
     $form['autofill_settings']['autofill'] = [
@@ -470,6 +677,53 @@ public function form(array $form, FormStateInterface $form_state) {
       '#webform_id' => $this->getEntity()->id(),
       '#default_value' => $settings['autofill_excluded_elements'],
     ];
+    $form['autofill_settings']['autofill_container']['token_tree_link'] = $this->tokenManager->buildTreeElement();
+
+    // Submission views.
+    $form['views_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Submission views settings'),
+      '#open' => TRUE,
+    ];
+    if (!$this->moduleHandler->moduleExists('webform_views')) {
+      $form['views_settings']['message'] = [
+        '#type' => 'webform_message',
+        '#message_type' => 'info',
+        '#message_message' => $this->t('To expose your webform elements to your webform submission views. Please install the <a href=":href">Webform Views Integration</a> module.', [':href' => 'https://www.drupal.org/project/webform_views']),
+        '#message_close' => TRUE,
+        '#message_storage' => WebformMessage::STORAGE_SESSION,
+      ];
+    }
+    if ($this->moduleHandler->moduleExists('views_ui')
+      && $this->currentUser()->hasPermission('administer views')
+      && ($view = View::load('webform_submissions'))
+      && ($view->access('duplicate'))) {
+
+      $form['views_settings']['submission_views_create'] = [
+        '#type' => 'link',
+        '#title' => $this->t('Create new submission view'),
+        '#url' => Url::fromRoute(
+          'entity.view.duplicate_form',
+          ['view' => 'webform_submissions']
+        ),
+        '#attributes' => [
+          'target' => '_blank',
+          'class' => ['button', 'button-action', 'button--small'],
+        ],
+        '#prefix' => '<p>',
+        '#suffix' => '</p>',
+      ];
+    }
+    $form['views_settings']['submission_views'] = [
+      '#type' => 'webform_submission_views',
+      '#title' => $this->t('Submission views'),
+      '#title_display' => 'invisible',
+      '#default_value' => $settings['submission_views'],
+    ];
+    $form['views_settings']['submission_views_replace'] = [
+      '#type' => 'webform_submission_views_replace',
+      '#default_value' => $settings['submission_views_replace'],
+    ];
 
     $this->tokenManager->elementValidate($form);
 
@@ -501,11 +755,34 @@ public function save(array $form, FormStateInterface $form_state) {
     $next_serial = (int) $values['next_serial'];
     $max_serial = $webform_storage->getMaxSerial($webform);
     if ($next_serial < $max_serial) {
-      drupal_set_message($this->t('The next submission number was increased to @min to make it higher than existing submissions.', ['@min' => $max_serial]));
+      $this->messenger()->addStatus($this->t('The next submission number was increased to @min to make it higher than existing submissions.', ['@min' => $max_serial]));
       $next_serial = $max_serial;
     }
     $webform_storage->setNextSerial($webform, $next_serial);
 
+    // Limit total unique.
+    if (!empty($values['limit_total_unique'])) {
+      $values['limit_total'] = NULL;
+      $values['limit_total_interval'] = NULL;
+      $values['limit_total_message'] = '';
+      $values['entity_limit_total'] = NULL;
+      $values['entity_limit_total_interval'] = NULL;
+      $values['limit_user'] = NULL;
+      $values['limit_user_interval'] = NULL;
+      $values['limit_user_message'] = '';
+      $values['entity_limit_user'] = NULL;
+      $values['entity_limit_user_interval'] = NULL;
+    }
+
+    // Limit user unique.
+    if (!empty($values['limit_user_unique'])) {
+      $values['limit_user'] = NULL;
+      $values['limit_user_interval'] = NULL;
+      $values['limit_user_message'] = '';
+      $values['entity_limit_user'] = NULL;
+      $values['entity_limit_user_interval'] = NULL;
+    }
+
     // Remove main properties.
     unset(
       $values['next_serial']
diff --git a/web/modules/webform/src/EventSubscriber/WebformExceptionHtmlSubscriber.php b/web/modules/webform/src/EventSubscriber/WebformExceptionHtmlSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..a65cdea5cc8e1ebfc12879a481421f6855008b3f
--- /dev/null
+++ b/web/modules/webform/src/EventSubscriber/WebformExceptionHtmlSubscriber.php
@@ -0,0 +1,325 @@
+<?php
+
+namespace Drupal\webform\EventSubscriber;
+
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\Routing\RedirectDestinationInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+use Drupal\webform\Element\WebformHtmlEditor;
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Entity\WebformSubmission;
+use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformTokenManagerInterface;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
+
+/**
+ * Event subscriber to redirect to login form when webform settings instruct to.
+ */
+class WebformExceptionHtmlSubscriber extends DefaultExceptionHtmlSubscriber {
+
+  use StringTranslationTrait;
+
+  /**
+   * The current account.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * The configuration object factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * The webform token manager.
+   *
+   * @var \Drupal\webform\WebformTokenManagerInterface
+   */
+  protected $tokenManager;
+
+  /**
+   * The messenger.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
+   * Constructs a new WebformSubscriber.
+   *
+   * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
+   *   The HTTP kernel.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger service.
+   * @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination
+   *   The redirect destination service.
+   * @param \Symfony\Component\Routing\Matcher\UrlMatcherInterface $access_unaware_router
+   *   A router implementation which does not check access.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The current user.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The configuration object factory.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger.
+   * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
+   *   The webform token manager.
+   */
+  public function __construct(HttpKernelInterface $http_kernel, LoggerInterface $logger, RedirectDestinationInterface $redirect_destination, UrlMatcherInterface $access_unaware_router, AccountInterface $account, ConfigFactoryInterface $config_factory, RendererInterface $renderer, MessengerInterface $messenger, WebformTokenManagerInterface $token_manager) {
+    parent::__construct($http_kernel, $logger, $redirect_destination, $access_unaware_router);
+
+    $this->account = $account;
+    $this->configFactory = $config_factory;
+    $this->renderer = $renderer;
+    $this->messenger = $messenger;
+    $this->tokenManager = $token_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static function getPriority() {
+    // Execute before CustomPageExceptionHtmlSubscriber which is -50.
+    // @see \Drupal\Core\EventSubscriber\CustomPageExceptionHtmlSubscriber::getPriority
+    return -49;
+  }
+
+  /**
+   * Handles a 403 error for HTML.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The event to process.
+   */
+  public function on403(GetResponseForExceptionEvent $event) {
+    if ($event->getRequestType() != HttpKernelInterface::MASTER_REQUEST) {
+      return;
+    }
+
+    $this->on403RedirectEntityAccess($event);
+    $this->on403RedirectPrivateFileAccess($event);
+  }
+
+  /**
+   * Redirect to user login when access is denied to private webform file.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The event to process.
+   *
+   * @see webform_file_download()
+   * @see \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::accessFileDownload
+   */
+  public function on403RedirectPrivateFileAccess(GetResponseForExceptionEvent $event) {
+    $path = $event->getRequest()->getPathInfo();
+    // Make sure the user is trying to access a private webform file upload.
+    if (strpos($path, '/system/files/webform/') !== 0) {
+      return;
+    }
+
+    // Make private webform file upload is not a temporary file.
+    // @see \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::postSave
+    if (strpos($path, '/_sid_/') !== FALSE) {
+      return;
+    }
+
+    // Check that private file redirection is enabled.
+    if (!$this->configFactory->get('webform.settings')->get('file.file_private_redirect')) {
+      return;
+    }
+
+    $message = $this->configFactory->get('webform.settings')->get('file.file_private_redirect_message');
+    $this->redirectToLogin($event, $message);
+  }
+
+  /**
+   * Redirect to user login when access is denied for webform or submission.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The event to process.
+   */
+  public function on403RedirectEntityAccess(GetResponseForExceptionEvent $event) {
+    $url = Url::fromUserInput($event->getRequest()->getPathInfo());
+    if (!$url) {
+      return;
+    }
+
+    $route_parameters = $url->isRouted() ? $url->getRouteParameters() : [];
+    if (empty($route_parameters['webform']) && empty($route_parameters['webform_submission'])) {
+      return;
+    }
+
+    $config = $this->configFactory->get('webform.settings');
+
+    // If webform submission, handle login redirect.
+    if (!empty($route_parameters['webform_submission'])) {
+      $webform_submission = WebformSubmission::load($route_parameters['webform_submission']);
+      $webform = $webform_submission->getWebform();
+
+      $submission_access_denied_message = $webform->getSetting('submission_access_denied_message')
+        ?: $config->get('settings.default_submission_access_denied_message');
+
+      switch ($webform->getSetting('submission_access_denied')) {
+        case WebformInterface::ACCESS_DENIED_LOGIN:
+          $this->redirectToLogin($event, $submission_access_denied_message, $webform_submission);
+          break;
+
+        case WebformInterface::ACCESS_DENIED_PAGE:
+          // Must manually build access denied path so that base path is not
+          // included.
+          $this->makeSubrequest($event, '/admin/structure/webform/manage/' . $webform->id() . '/submission/' . $webform_submission->id() . '/access-denied', Response::HTTP_FORBIDDEN);
+          break;
+
+        case WebformInterface::ACCESS_DENIED_DEFAULT:
+        default:
+          // Make the default 403 request so that we can add cacheable dependencies.
+          $this->makeSubrequest($event, $this->getSystemSite403Path(), Response::HTTP_FORBIDDEN);
+          break;
+      }
+
+      // Add cacheable dependencies.
+      $response = $event->getResponse();
+      if ($response instanceof CacheableResponseInterface) {
+        $response->addCacheableDependency($webform);
+        $response->addCacheableDependency($webform_submission);
+        $response->addCacheableDependency($config);
+      }
+      return;
+    }
+
+    // If webform, handle access denied redirect or page.
+    if (!empty($route_parameters['webform'])) {
+      $webform = Webform::load($route_parameters['webform']);
+
+      $webform_access_denied_message = $webform->getSetting('form_access_denied_message')
+        ?: $config->get('settings.default_form_access_denied_message');
+
+      switch ($webform->getSetting('form_access_denied')) {
+        case WebformInterface::ACCESS_DENIED_LOGIN:
+          $this->redirectToLogin($event, $webform_access_denied_message, $webform);
+          break;
+
+        case WebformInterface::ACCESS_DENIED_PAGE:
+          // Must manually build access denied path so that base path is not
+          // included.
+          $this->makeSubrequest($event, '/webform/' . $webform->id() . '/access-denied', Response::HTTP_FORBIDDEN);
+          break;
+
+        case WebformInterface::ACCESS_DENIED_MESSAGE:
+          // Display message.
+          $this->setMessage($webform_access_denied_message, $webform);
+          // Make the default 403 request so that we can add cacheable dependencies.
+          $this->makeSubrequest($event, $this->getSystemSite403Path(), Response::HTTP_FORBIDDEN);
+          break;
+
+        case WebformInterface::ACCESS_DENIED_DEFAULT:
+        default:
+          // Make the default 403 request so that we can add cacheable dependencies.
+          $this->makeSubrequest($event, $this->getSystemSite403Path(), Response::HTTP_FORBIDDEN);
+          break;
+      }
+      // Add cacheable dependencies.
+      $response = $event->getResponse();
+      if ($response instanceof CacheableResponseInterface) {
+        $response->addCacheableDependency($webform);
+        $response->addCacheableDependency($config);
+      }
+      return;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getHandledFormats() {
+    return ['html'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onException(GetResponseForExceptionEvent $event) {
+    // Only handle 403 exception.
+    // @see \Drupal\webform\EventSubscriber\WebformExceptionHtmlSubscriber::on403
+    $exception = $event->getException();
+    if ($exception instanceof HttpExceptionInterface && $exception->getStatusCode() === 403) {
+      parent::onException($event);
+    }
+  }
+
+  /**
+   * Get 403 path from system.site config.
+   *
+   * @return string
+   *   The custom 403 path or Drupal's default 403 path.
+   */
+  protected function getSystemSite403Path() {
+    return $this->configFactory->get('system.site')->get('page.403') ?: '/system/403';
+  }
+
+  /**
+   * Redirect to user login with destination and display custom message.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The event to process.
+   * @param null|string $message
+   *   (Optional) Message to be display on user login.
+   * @param null|\Drupal\Core\Entity\EntityInterface $entity
+   *   (Optional) Entity to be used when replacing tokens.
+   */
+  protected function redirectToLogin(GetResponseForExceptionEvent $event, $message = NULL, EntityInterface $entity = NULL) {
+    // Display message.
+    if ($message) {
+      $this->setMessage($message, $entity);
+    }
+
+    // Only redirect anonymous users.
+    if ($this->account->isAuthenticated()) {
+      return;
+    }
+
+    $redirect_url = Url::fromRoute(
+      'user.login',
+      [],
+      ['absolute' => TRUE, 'query' => $this->redirectDestination->getAsArray()]
+    );
+    $event->setResponse(new RedirectResponse($redirect_url->toString()));
+  }
+
+  /**
+   * Display custom message.
+   *
+   * @param null|string $message
+   *   (Optional) Message to be display on user login.
+   * @param null|\Drupal\Core\Entity\EntityInterface $entity
+   *   (Optional) Entity to be used when replacing tokens.
+   */
+  protected function setMessage($message, EntityInterface $entity = NULL) {
+    $message = $this->tokenManager->replace($message, $entity);
+    $build = WebformHtmlEditor::checkMarkup($message);
+    $this->messenger->addStatus($this->renderer->renderPlain($build));
+  }
+
+}
diff --git a/web/modules/webform/src/EventSubscriber/WebformSubscriber.php b/web/modules/webform/src/EventSubscriber/WebformSubscriber.php
deleted file mode 100644
index 979a068f02d7eedbec80743fa24419061c46e660..0000000000000000000000000000000000000000
--- a/web/modules/webform/src/EventSubscriber/WebformSubscriber.php
+++ /dev/null
@@ -1,220 +0,0 @@
-<?php
-
-namespace Drupal\webform\EventSubscriber;
-
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Routing\RedirectDestinationInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Render\RendererInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\Url;
-use Drupal\webform\Element\WebformHtmlEditor;
-use Drupal\webform\Entity\Webform;
-use Drupal\webform\Entity\WebformSubmission;
-use Drupal\webform\WebformTokenManagerInterface;
-use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
-use Symfony\Component\HttpKernel\HttpKernelInterface;
-use Symfony\Component\HttpKernel\KernelEvents;
-use Symfony\Component\HttpFoundation\Response;
-
-/**
- * Event subscriber to redirect to login form when webform settings instruct to.
- */
-class WebformSubscriber implements EventSubscriberInterface {
-
-  use StringTranslationTrait;
-
-  /**
-   * The current account.
-   *
-   * @var \Drupal\Core\Session\AccountInterface
-   */
-  protected $account;
-
-  /**
-   * The configuration object factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
-  /**
-   * The renderer.
-   *
-   * @var \Drupal\Core\Render\RendererInterface
-   */
-  protected $renderer;
-
-  /**
-   * The redirect.destination service.
-   *
-   * @var \Drupal\Core\Routing\RedirectDestinationInterface
-   */
-  protected $redirectDestination;
-
-  /**
-   * The webform token manager.
-   *
-   * @var \Drupal\webform\WebformTokenManagerInterface
-   */
-  protected $tokenManager;
-
-  /**
-   * Constructs a new WebformSubscriber.
-   *
-   * @param \Drupal\Core\Session\AccountInterface $account
-   *   The current user.
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The configuration object factory.
-   * @param \Drupal\Core\Render\RendererInterface $renderer
-   *   The renderer.
-   * @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination
-   *   The redirect.destination service.
-   * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
-   *   The webform token manager.
-   */
-  public function __construct(AccountInterface $account, ConfigFactoryInterface $config_factory, RendererInterface $renderer, RedirectDestinationInterface $redirect_destination, WebformTokenManagerInterface $token_manager) {
-    $this->account = $account;
-    $this->configFactory = $config_factory;
-    $this->renderer = $renderer;
-    $this->redirectDestination = $redirect_destination;
-
-    $this->tokenManager = $token_manager;
-  }
-
-  /**
-   * Redirect to user login when access is denied to private webform file.
-   *
-   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
-   *   The event to process.
-   *
-   * @see webform_file_download()
-   * @see \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::accessFileDownload
-   */
-  public function onRespondRedirectPrivateFileAccess(FilterResponseEvent $event) {
-    if ($event->getRequestType() != HttpKernelInterface::MASTER_REQUEST) {
-      return;
-    }
-
-    // Check for 403 access denied status code in (master) response.
-    $response = $event->getResponse();
-    if ($response->getStatusCode() != Response::HTTP_FORBIDDEN) {
-      return;
-    }
-
-    $path = $event->getRequest()->getPathInfo();
-    // Make sure the user is trying to access a private webform file upload.
-    if (strpos($path, '/system/files/webform/') !== 0) {
-      return;
-    }
-
-    // Make private webform file upload is not a temporary file.
-    // @see \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::postSave
-    if (strpos($path, '/_sid_/') !== FALSE) {
-      return;
-    }
-
-    // Only redirect anonymous users.
-    if ($this->account->isAuthenticated()) {
-      return;
-    }
-
-    // Check that private file redirection is enabled.
-    if (!$this->configFactory->get('webform.settings')->get('file.file_private_redirect')) {
-      return;
-    }
-
-    $message = $this->configFactory->get('webform.settings')->get('file.file_private_redirect_message');
-    $this->redirectToLogin($event, $message);
-  }
-
-  /**
-   * Redirect to user login when access is denied for webform or submission.
-   *
-   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
-   *   The event to process.
-   */
-  public function onRespondRedirectEntityAccess(FilterResponseEvent $event) {
-    if ($event->getRequestType() != HttpKernelInterface::MASTER_REQUEST) {
-      return;
-    }
-
-    // Check for 403 access denied status code in (master) response.
-    $response = $event->getResponse();
-    if ($response->getStatusCode() != Response::HTTP_FORBIDDEN) {
-      return;
-    }
-
-    $url = Url::fromUserInput($event->getRequest()->getPathInfo());
-    if (!$url) {
-      return;
-    }
-
-    $route_parameters = $url->isRouted() ? $url->getRouteParameters() : [];
-    if (empty($route_parameters['webform']) && empty($route_parameters['webform_submission'])) {
-      return;
-    }
-
-    // If webform submission, handle login redirect.
-    if (!empty($route_parameters['webform_submission'])) {
-      $webform_submission = WebformSubmission::load($route_parameters['webform_submission']);
-      if ($webform_submission->getWebform()->getSetting('submission_login')) {
-        $message = $webform_submission->getWebform()->getSetting('submission_login_message')
-          ?: $this->configFactory->get('webform.settings')->get('settings.default_submission_login_message');
-        $this->redirectToLogin($event, $message, $webform_submission);
-      };
-      return;
-    }
-
-    // If webform, handle login redirect.
-    if (!empty($route_parameters['webform'])) {
-      $webform = Webform::load($route_parameters['webform']);
-      if ($webform->getSetting('form_login')) {
-        $message = $webform->getSetting('form_login_message')
-          ?: $this->configFactory->get('webform.settings')->get('settings.default_form_login_message');
-        $this->redirectToLogin($event, $message, $webform);
-      };
-      return;
-    }
-  }
-
-  /**
-   * Redirect to user login with destination and display custom message.
-   *
-   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
-   *   The event to process.
-   * @param null|string $message
-   *   (Optional) Message to be display on user login.
-   * @param null|\Drupal\Core\Entity\EntityInterface $entity
-   *   (Optional) Entity to be used when replacing tokens.
-   */
-  protected function redirectToLogin(FilterResponseEvent $event, $message = NULL, EntityInterface $entity = NULL) {
-    // Display message.
-    if ($message) {
-      $message = $this->tokenManager->replace($message, $entity);
-      $build = WebformHtmlEditor::checkMarkup($message);
-      drupal_set_message($this->renderer->renderPlain($build));
-    }
-
-    $redirect_url = Url::fromRoute(
-      'user.login',
-      [],
-      ['absolute' => TRUE, 'query' => $this->redirectDestination->getAsArray()]
-    );
-    $event->setResponse(new RedirectResponse($redirect_url->toString()));
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function getSubscribedEvents() {
-    $events = [];
-    $events[KernelEvents::RESPONSE][] = ['onRespondRedirectPrivateFileAccess'];
-    $events[KernelEvents::RESPONSE][] = ['onRespondRedirectEntityAccess'];
-    return $events;
-  }
-
-}
diff --git a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigAdvancedForm.php b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigAdvancedForm.php
index ad04f0e6e7fc8a3f075981f826b2442a881a7d2f..03b22da034bac8de0a22e40fb3a01f0d20ff18a7 100644
--- a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigAdvancedForm.php
+++ b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigAdvancedForm.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Routing\RouteBuilderInterface;
 use Drupal\Core\Url;
+use Drupal\webform\Commands\WebformCliService;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -36,6 +37,13 @@ class WebformAdminConfigAdvancedForm extends WebformAdminConfigBaseForm {
    */
   protected $routerBuilder;
 
+  /**
+   * The (drush) command-line service.
+   *
+   * @var \Drupal\webform\Commands\WebformCliService
+   */
+  protected $cliService;
+
   /**
    * {@inheritdoc}
    */
@@ -54,12 +62,15 @@ public function getFormId() {
    *   The render cache service.
    * @param \Drupal\Core\Routing\RouteBuilderInterface $router_builder
    *   The router builder service.
+   * @param \Drupal\webform\Commands\WebformCliService $cli_service
+   *   The (drush) command-line service.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, CacheBackendInterface $render_cache, RouteBuilderInterface $router_builder) {
+  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, CacheBackendInterface $render_cache, RouteBuilderInterface $router_builder, WebformCliService $cli_service) {
     parent::__construct($config_factory);
     $this->renderCache = $render_cache;
     $this->moduleHandler = $module_handler;
     $this->routerBuilder = $router_builder;
+    $this->cliService = $cli_service;
   }
 
   /**
@@ -70,7 +81,8 @@ public static function create(ContainerInterface $container) {
       $container->get('config.factory'),
       $container->get('module_handler'),
       $container->get('cache.render'),
-      $container->get('router.builder')
+      $container->get('router.builder'),
+      $container->get('webform.cli_service')
     );
   }
 
@@ -115,6 +127,13 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#return_value' => TRUE,
       '#default_value' => $config->get('ui.details_save'),
     ];
+    $form['ui']['help_disabled'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Disable help'),
+      '#description' => $this->t('If checked, help text will be removed from every webform page and form.'),
+      '#return_value' => TRUE,
+      '#default_value' => $config->get('ui.help_disabled'),
+    ];
     $form['ui']['dialog_disabled'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Disable dialogs'),
@@ -128,7 +147,6 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#description' => $this->t('If checked, all off-canvas system trays will be disabled.'),
       '#return_value' => TRUE,
       '#default_value' => $config->get('ui.offcanvas_disabled'),
-      '#access' => (floatval(\Drupal::VERSION) >= 8.5),
       '#states' => [
         'visible' => [
           ':input[name="ui[dialog_disabled]"]' => [
@@ -157,7 +175,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     // Requirements.
     $form['requirements'] = [
       '#type' => 'details',
-      '#title' => $this->t('Requirements'),
+      '#title' => $this->t('Requirement settings'),
       '#description' => $this->t('The below requirements are checked by the <a href=":href">Status report</a>.', [':href' => Url::fromRoute('system.status')->toString()]),
       '#open' => TRUE,
       '#tree' => TRUE,
@@ -219,6 +237,15 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#min' => 1,
       '#required' => TRUE,
       '#default_value' => $config->get('batch.default_batch_export_size'),
+      '#description' => $this->t('Batch export size is used when submissions are being exported/downloaded.'),
+    ];
+    $form['batch']['default_batch_import_size'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Batch import size'),
+      '#min' => 1,
+      '#required' => TRUE,
+      '#default_value' => $config->get('batch.default_batch_import_size'),
+      '#description' => $this->t('Batch import size is used when submissions are being imported/uploaded.'),
     ];
     $form['batch']['default_batch_update_size'] = [
       '#type' => 'number',
@@ -226,10 +253,12 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#min' => 1,
       '#required' => TRUE,
       '#default_value' => $config->get('batch.default_batch_update_size'),
+      '#description' => $this->t('Batch update size is used when submissions are being bulk updated.'),
     ];
     $form['batch']['default_batch_delete_size'] = [
       '#type' => 'number',
       '#title' => $this->t('Batch delete size'),
+      '#description' => $this->t('Batch delete size is used when submissions are being cleared.'),
       '#min' => 1,
       '#required' => TRUE,
       '#default_value' => $config->get('batch.default_batch_delete_size'),
@@ -243,6 +272,25 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#default_value' => $config->get('batch.default_batch_email_size'),
     ];
 
+    // Repair.
+    $form['repair'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Repair webform configuration'),
+      '#open' => TRUE,
+      '#description' => '<p>' . $this->t('If older Webform configuration files are imported after the Webform module has been updated this may cause the older configuration to be out-of-sync and result in unexpected behaviors and errors.') . '</p>' .
+        '<p>' . $this->t("Running the below 'Repair' command will apply all missing settings to older Webform configuration files.") . '</p>',
+      '#help' => FALSE,
+      '#weight' => 100,
+    ];
+    $form['repair']['action'] = ['#type' => 'actions'];
+    $form['repair']['action']['repair_configuration'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Repair configuration'),
+      '#attributes' => [
+        'onclick' => 'return confirm("' . $this->t('Are you sure you want to repair webform configuration?') . '\n' . $this->t('This cannot be undone!!!') . '");',
+      ],
+    ];
+
     return parent::buildForm($form, $form_state);
   }
 
@@ -250,19 +298,62 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
-    $config = $this->config('webform.settings');
-    $config->set('ui', $form_state->getValue('ui'));
-    $config->set('requirements', $form_state->getValue('requirements'));
-    $config->set('test', $form_state->getValue('test'));
-    $config->set('batch', $form_state->getValue('batch'));
-    $config->save();
+    $op = (string) $form_state->getValue('op');
+    if ($op === (string) $this->t('Repair configuration')) {
+      // Copied from:
+      // @see \Drupal\webform\Commands\WebformCliService::drush_webform_repair
+      module_load_include('install', 'webform');
+
+      $this->messenger()->addMessage($this->t('Repairing webform submission storage schema…'));
+      _webform_update_webform_submission_storage_schema();
+
+      $this->messenger()->addMessage($this->t('Repairing admin settings…'));
+      _webform_update_admin_settings(TRUE);
+
+      $this->messenger()->addMessage($this->t('Repairing webform settings…'));
+      _webform_update_webform_settings();
+
+      $this->messenger()->addMessage($this->t('Repairing webform handlers…'));
+      _webform_update_webform_handler_settings();
+
+      $this->messenger()->addMessage($this->t('Repairing webform field storage definitions…'));
+      _webform_update_field_storage_definitions();
+
+      $this->messenger()->addMessage($this->t('Repairing webform submission storage schema…'));
+      _webform_update_webform_submission_storage_schema();
+
+      drupal_flush_all_caches();
+
+      $this->messenger()->addStatus($this->t('Webform configuration has been repaired.'));
+    }
+    else {
+      // Update config and submit form.
+      $config = $this->config('webform.settings');
+      $config->set('ui', $form_state->getValue('ui'));
+      $config->set('requirements', $form_state->getValue('requirements'));
+      $config->set('test', $form_state->getValue('test'));
+      $config->set('batch', $form_state->getValue('batch'));
+
+      // Track if help is disabled.
+      // @todo Figure out how to clear cached help block.
+      $is_help_disabled = ($config->getOriginal('ui.help_disabled') != $config->get('ui.help_disabled'));
 
-    // Clear render cache so that local tasks can be updated.
-    // @see webform_local_tasks_alter()
-    $this->renderCache->deleteAll();
-    $this->routerBuilder->rebuild();
+      parent::submitForm($form, $form_state);
 
-    parent::submitForm($form, $form_state);
+      // Clear cached data.
+      if ($is_help_disabled) {
+        // Flush cache when help is being enabled.
+        // @see webform_help()
+        drupal_flush_all_caches();
+      }
+      else {
+        // Clear render cache so that local tasks can be updated to hide/show
+        // the 'Contribute' tab.
+        // @see webform_local_tasks_alter()
+        $this->renderCache->deleteAll();
+        $this->routerBuilder->rebuild();
+      }
+    }
   }
 
 }
diff --git a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigBaseForm.php b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigBaseForm.php
index 71ce6fc8c45145e3ab52b0b2352e710c3c9c48ee..d4a72f271856cbad312fff26e4692e19180a5c34 100644
--- a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigBaseForm.php
+++ b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigBaseForm.php
@@ -3,7 +3,12 @@
 namespace Drupal\webform\Form\AdminConfig;
 
 use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Config\Config;
 use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Plugin\WebformElement\TableSelect;
+use Drupal\webform\Plugin\WebformElementManagerInterface;
+use Drupal\webform\Plugin\WebformHandlerManager;
 
 /**
  * Base webform admin settings form.
@@ -17,8 +22,19 @@ protected function getEditableConfigNames() {
     return ['webform.settings'];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $config = $this->config('webform.settings');
+    _webform_config_update($config);
+    $config->save();
+
+    parent::submitForm($form, $form_state);
+  }
+
   /****************************************************************************/
-  // Exclude plugins
+  // Exclude plugins.
   /****************************************************************************/
 
   /**
@@ -33,9 +49,6 @@ protected function getEditableConfigNames() {
    *   A table select element used to excluded plugins by id.
    */
   protected function buildExcludedPlugins(PluginManagerInterface $plugin_manager, array $excluded_ids) {
-    $plugins = $plugin_manager->getDefinitions();
-    $plugins = $plugin_manager->getSortedDefinitions($plugins);
-
     $header = [
       'title' => ['data' => $this->t('Title')],
       'id' => ['data' => $this->t('Name'), 'class' => [RESPONSIVE_PRIORITY_LOW]],
@@ -44,22 +57,39 @@ protected function buildExcludedPlugins(PluginManagerInterface $plugin_manager,
 
     $ids = [];
     $options = [];
+    $plugins = $this->getPluginDefinitions($plugin_manager);
     foreach ($plugins as $id => $plugin_definition) {
       $ids[$id] = $id;
+
+      $description = [
+        'data' => [
+          'content' => ['#markup' => $plugin_definition['description']],
+        ],
+      ];
+      if (!empty($plugin_definition['deprecated'])) {
+        $description['data']['deprecated'] = [
+          '#type' => 'webform_message',
+          '#message_message' => $plugin_definition['deprecated_message'],
+          '#message_type' => 'warning',
+        ];
+      }
       $options[$id] = [
         'title' => $plugin_definition['label'],
         'id' => $plugin_definition['id'],
-        'description' => $plugin_definition['description'],
+        'description' => $description,
       ];
     }
 
-    return [
+    $element = [
       '#type' => 'tableselect',
       '#header' => $header,
       '#options' => $options,
       '#required' => TRUE,
+      '#sticky' => TRUE,
       '#default_value' => array_diff($ids, $excluded_ids),
     ];
+    TableSelect::setProcessTableSelectCallback($element);
+    return $element;
   }
 
   /**
@@ -76,10 +106,8 @@ protected function buildExcludedPlugins(PluginManagerInterface $plugin_manager,
    * @see \Drupal\webform\Form\WebformAdminSettingsForm::buildExcludedPlugins
    */
   protected function convertIncludedToExcludedPluginIds(PluginManagerInterface $plugin_manager, array $included_ids) {
-    $plugins = $plugin_manager->getDefinitions();
-    $plugins = $plugin_manager->getSortedDefinitions($plugins);
-
     $ids = [];
+    $plugins = $this->getPluginDefinitions($plugin_manager);
     foreach ($plugins as $id => $plugin) {
       $ids[$id] = $id;
     }
@@ -89,4 +117,25 @@ protected function convertIncludedToExcludedPluginIds(PluginManagerInterface $pl
     return $excluded_ids;
   }
 
+  /**
+   * Get plugin definitions.
+   *
+   * @param \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager
+   *   A webform element, handler, or exporter plugin manager.
+   *
+   * @return array
+   *   Plugin definitions.
+   */
+  protected function getPluginDefinitions(PluginManagerInterface $plugin_manager) {
+    $plugins = $plugin_manager->getDefinitions();
+    $plugins = $plugin_manager->getSortedDefinitions($plugins);
+    if ($plugin_manager instanceof WebformElementManagerInterface) {
+      unset($plugins['webform_element']);
+    }
+    elseif ($plugin_manager instanceof WebformHandlerManager) {
+      unset($plugins['broken']);
+    }
+    return $plugins;
+  }
+
 }
diff --git a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigElementsForm.php b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigElementsForm.php
index 2aed78a3895014172aae2f016206fa356a0c124b..aa7e3735ea211c29653039ca95055a3fe4ae9c4a 100644
--- a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigElementsForm.php
+++ b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigElementsForm.php
@@ -2,16 +2,18 @@
 
 namespace Drupal\webform\Form\AdminConfig;
 
+use Drupal\Component\Utility\Bytes;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Url;
 use Drupal\file\Plugin\Field\FieldType\FileItem;
+use Drupal\webform\Element\WebformMessage;
 use Drupal\webform\Utility\WebformArrayHelper;
-use Drupal\webform\Plugin\WebformElementManagerInterface;
 use Drupal\webform\Utility\WebformOptionsHelper;
+use Drupal\webform\Plugin\WebformElementManagerInterface;
 use Drupal\webform\WebformLibrariesManagerInterface;
+use Drupal\webform\WebformTokenManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -26,6 +28,13 @@ class WebformAdminConfigElementsForm extends WebformAdminConfigBaseForm {
    */
   protected $moduleHandler;
 
+  /**
+   * The webform token manager.
+   *
+   * @var \Drupal\webform\WebformTokenManagerInterface
+   */
+  protected $tokenManager;
+
   /**
    * The webform element manager.
    *
@@ -54,14 +63,17 @@ public function getFormId() {
    *   The factory for configuration objects.
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler.
+   * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
+   *   The webform token manager.
    * @param \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager
    *   The webform element manager.
    * @param \Drupal\webform\WebformLibrariesManagerInterface $libraries_manager
    *   The webform libraries manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, WebformElementManagerInterface $element_manager, WebformLibrariesManagerInterface $libraries_manager) {
+  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, WebformTokenManagerInterface $token_manager, WebformElementManagerInterface $element_manager, WebformLibrariesManagerInterface $libraries_manager) {
     parent::__construct($config_factory);
     $this->moduleHandler = $module_handler;
+    $this->tokenManager = $token_manager;
     $this->elementManager = $element_manager;
     $this->librariesManager = $libraries_manager;
   }
@@ -73,6 +85,7 @@ public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
       $container->get('module_handler'),
+      $container->get('webform.token_manager'),
       $container->get('plugin.manager.webform.element'),
       $container->get('webform.libraries_manager')
     );
@@ -87,7 +100,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     // Element: Settings.
     $form['element'] = [
       '#type' => 'details',
-      '#title' => $this->t('Element settings'),
+      '#title' => $this->t('Element general settings'),
       '#open' => TRUE,
       '#tree' => TRUE,
     ];
@@ -101,7 +114,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#type' => 'webform_radios_other',
       '#title' => $this->t('Allowed tags'),
       '#options' => [
-        'admin' => $this->t('Admin tags Excludes: script, iframe, etc...'),
+        'admin' => $this->t('Admin tags Excludes: script, iframe, etc…'),
         'html' => $this->t('HTML tags: Includes only @html_tags.', ['@html_tags' => WebformArrayHelper::toString(Xss::getHtmlTagList())]),
       ],
       '#other__option_label' => $this->t('Custom tags'),
@@ -150,7 +163,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $form['element']['default_more_title'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Default more label'),
-      '#description' => $this->t('The (read) more label used hide/show more information about an element.'),
+      '#description' => $this->t('The (read) more label used to hide/show more information about an element.'),
       '#required' => 'required',
       '#default_value' => $config->get('element.default_more_title'),
     ];
@@ -252,36 +265,83 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         $format_options[$filter->id()] = $filter->label();
       }
     }
-    $form['html_editor']['format'] = [
+    $form['html_editor']['format_container'] = [
+      '#type' => 'container',
+      '#states' => [
+        'visible' => [
+          ':input[name="html_editor[disabled]"]' => ['checked' => FALSE],
+        ],
+      ],
+    ];
+    $form['html_editor']['format_container']['element_format'] = [
       '#type' => 'select',
-      '#title' => $this->t('Text format'),
+      '#title' => $this->t('Element text format'),
       '#description' => $this->t('Leave blank to use the custom and recommended Webform specific HTML editor.'),
       '#empty_option' => $this->t('- None -'),
       '#options' => $format_options,
-      '#default_value' => $config->get('html_editor.format'),
+      '#default_value' => $config->get('html_editor.element_format'),
+      '#parents' => ['html_editor', 'element_format'],
+    ];
+    $form['html_editor']['format_container']['mail_format'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Mail text format'),
+      '#description' => $this->t('Leave blank to use the custom and recommended Webform specific HTML editor.'),
+      '#empty_option' => $this->t('- None -'),
+      '#options' => $format_options,
+      '#default_value' => $config->get('html_editor.mail_format'),
+      '#parents' => ['html_editor', 'mail_format'],
       '#states' => [
         'visible' => [
           ':input[name="html_editor[disabled]"]' => ['checked' => FALSE],
         ],
       ],
     ];
-    $t_args = [
-      ':dialog_href' => Url::fromRoute('<current>', [], ['fragment' => 'edit-ui'])->toString(),
-      ':modules_href' => Url::fromRoute('system.modules_list', [], ['fragment' => 'edit-modules-core-experimental'])->toString(),
+    $form['html_editor']['format_container']['make_unused_managed_files_temporary'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Unused html editor files should be marked temporary'),
+      '#description' => $this->t('Drupal core does not automatically delete unused files because unused files could reused.'),
+      '#return_value' => TRUE,
+      '#default_value' => $config->get('html_editor.make_unused_managed_files_temporary'),
+      '#parents' => ['html_editor', 'make_unused_managed_files_temporary'],
+      '#states' => [
+        'visible' => [
+          [':input[name="html_editor[element_format]"]' => ['!value' => '']],
+          'or',
+          [':input[name="html_editor[mail_format]"]' => ['!value' => '']],
+        ],
+      ],
     ];
-    $form['html_editor']['message'] = [
+    $form['html_editor']['format_container']['warning_message'] = [
       '#type' => 'webform_message',
-      '#message_message' => $this->t('Text formats that open CKEditor image and/or link dialogs will not work properly.') . '<br />' .
-        $this->t('You may need to <a href=":dialog_href">disable dialogs</a> or enable the experimental <a href=":modules_href">Settings Tray</a> module.', $t_args) . '<br />' .
-        $this->t('For more information see: <a href="https://www.drupal.org/node/2741877">Issue #2741877: Nested modals don\'t work</a>'),
+      '#message_message' => $this->t('Files uploaded via the CKEditor file dialog to webform elements, settings, and configuration will not be exportable.') . '<br/>' .
+        '<strong>' . $this->t('All files must be uploaded to your production environment and then copied to development and local environment.') . '</strong>',
       '#message_type' => 'warning',
       '#states' => [
         'visible' => [
-          ':input[name="html_editor[disabled]"]' => ['checked' => FALSE],
-          ':input[name="html_editor[format]"]' => ['!value' => ''],
+          [':input[name="html_editor[element_format]"]' => ['!value' => '']],
+          'or',
+          [':input[name="html_editor[mail_format]"]' => ['!value' => '']],
         ],
       ],
-    ];
+      '#message_close' => TRUE,
+      '#message_storage' => WebformMessage::STORAGE_SESSION,
+    ];
+    if (!$this->moduleHandler->moduleExists('imce')) {
+      $form['html_editor']['format_container']['help_message'] = [
+        '#type' => 'webform_message',
+        '#message_message' => $this->t('It is recommended to use the <a href=":href">IMCE module</a> to manage webform elements, settings, and configuration files.', [':href' => 'https://www.drupal.org/project/imce']),
+        '#message_type' => 'info',
+        '#states' => [
+          'visible' => [
+            [':input[name="html_editor[element_format]"]' => ['!value' => '']],
+            'or',
+            [':input[name="html_editor[mail_format]"]' => ['!value' => '']],
+          ],
+        ],
+        '#message_close' => TRUE,
+        '#message_storage' => WebformMessage::STORAGE_SESSION,
+      ];
+    }
 
     // Element: Location.
     $form['location'] = [
@@ -289,6 +349,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Location settings'),
       '#open' => TRUE,
       '#tree' => TRUE,
+      '#access' => $this->librariesManager->isIncluded('jquery.geocomplete') || $this->librariesManager->isIncluded('algolia.places'),
     ];
     $form['location']['default_google_maps_api_key'] = [
       '#type' => 'textfield',
@@ -297,6 +358,20 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#default_value' => $config->get('element.default_google_maps_api_key'),
       '#access' => $this->librariesManager->isIncluded('jquery.geocomplete'),
     ];
+    $form['location']['default_algolia_places_app_id'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Algolia application id'),
+      '#description' => $this->t('Algolia requires users to use a valid application id and API key for more than 1,000 requests per day. By <a href="https://www.algolia.com/users/sign_up/places">signing up</a>, you can create a free Places app and access your API keys.'),
+      '#default_value' => $config->get('element.default_algolia_places_app_id'),
+      '#access' => $this->librariesManager->isIncluded('algolia.places'),
+    ];
+    $form['location']['default_algolia_places_api_key'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Algolia API key'),
+      '#default_value' => $config->get('element.default_algolia_places_api_key'),
+      '#access' => $this->librariesManager->isIncluded('algolia.places'),
+    ];
+
     // Element: Select.
     $form['select'] = [
       '#type' => 'details',
@@ -342,7 +417,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
     $form['file']['delete_temporary_managed_files'] = [
       '#type' => 'checkbox',
-      '#title' => $this->t('Immediately deleted temporary managed files'),
+      '#title' => $this->t('Immediately delete temporary managed files'),
       '#description' => $this->t('Drupal core does not immediately delete temporary file. For webform submissions it is recommended that temporary files are immediately deleted.'),
       '#return_value' => TRUE,
       '#default_value' => $config->get('file.delete_temporary_managed_files'),
@@ -373,15 +448,24 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         ],
       ],
     ];
-
     $form['file']['default_max_filesize'] = [
       '#type' => 'textfield',
-      '#title' => $this->t('Default maximum upload size'),
-      '#description' => $this->t('Enter a value like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes) in order to restrict the allowed file size. If left empty the file sizes will be limited only by PHP\'s maximum post and file upload sizes (current limit <strong>%limit</strong>).', ['%limit' => function_exists('file_upload_max_size') ? format_size(file_upload_max_size()) : $this->t('N/A')]),
+      '#title' => $this->t('Default maximum file upload size'),
+      '#description' => $this->t('Enter a value like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes) in order to restrict the allowed file size. If left empty the file sizes will be limited only by PHP\'s maximum post and file upload sizes.')
+        . '<br /><br />'
+        . $this->t('Current limit: %limit', ['%limit' => function_exists('file_upload_max_size') ? format_size(file_upload_max_size()) : $this->t('N/A')]),
       '#element_validate' => [[get_class($this), 'validateMaxFilesize']],
       '#size' => 10,
       '#default_value' => $config->get('file.default_max_filesize'),
     ];
+    $form['file']['default_form_file_limit'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Default file upload limit per form'),
+      '#description' => $this->t('Enter a value like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes) in order to set file upload limit.'),
+      '#element_validate' => [[get_class($this), 'validateMaxFilesize']],
+      '#size' => 10,
+      '#default_value' => $config->get('settings.default_form_file_limit'),
+    ];
     $file_types = [
       'managed_file' => 'file',
       'audio_file' => 'audio file',
@@ -400,6 +484,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         '#default_value' => $config->get("file.default_{$file_type_name}_extensions"),
       ];
     }
+    $form['file']['token_tree_link'] = $this->tokenManager->buildTreeElement();
 
     // Element: (Excluded) Types.
     $form['types'] = [
@@ -416,25 +501,19 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $form['types']['excluded_elements']['#header']['description']['width'] = '50%';
     // Add warning to all password elements.
     foreach ($form['types']['excluded_elements']['#options'] as $element_type => &$excluded_element_option) {
-      if (strpos($element_type,'password') !== FALSE) {
-        $excluded_element_option['description'] = [
-          'data' => [
-            'description' => ['#markup' => $excluded_element_option['description']],
-            'message' => [
-              '#type' => 'webform_message',
-              '#message_type' => 'warning',
-              '#message_message' => $this->t('Webform submissions store passwords as plain text.') . ' ' .
-                $this->t('Any webform that includes this element should enable <a href=":href">encryption</a>.', [':href' => 'https://www.drupal.org/project/webform_encrypt']),
-              '#attributes' => ['class' => ['js-form-wrapper']],
-              '#states' => [
-                'visible' => [
-                  ':input[name="excluded_elements[' . $element_type . ']"]' => ['checked' => TRUE],
-                ],
-              ],
+      if (strpos($element_type, 'password') !== FALSE) {
+        $excluded_element_option['description']['data']['message'] = [
+          '#type' => 'webform_message',
+          '#message_type' => 'warning',
+          '#message_message' => $this->t('Webform submissions store passwords as plain text.') . ' ' .
+            $this->t('Any webform that includes this element should enable <a href=":href">encryption</a>.', [':href' => 'https://www.drupal.org/project/webform_encrypt']),
+          '#attributes' => ['class' => ['js-form-wrapper']],
+          '#states' => [
+            'visible' => [
+              ':input[name="excluded_elements[' . $element_type . ']"]' => ['checked' => TRUE],
             ],
           ],
         ];
-
       }
     }
 
@@ -492,7 +571,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
           '#title' => $this->t('Items format'),
           '#title_display' => 'invisible',
           '#field_suffix' => [
-            '#type' => 'webform_help',
+            '#help_title' => $element_plugin_label,
             '#help' => $this->t('Defaults to: %value', ['%value' => $items_default_format_label]),
           ],
           '#empty_option' => $this->t('- Default -'),
@@ -516,6 +595,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         'item' => ['data' => $this->t('Item format'), 'width' => '25%'],
         'items' => ['data' => $this->t('Items format'), 'width' => '25%'],
       ],
+      '#sticky' => TRUE,
     ] + $rows;
 
     return parent::buildForm($form, $form_state);
@@ -535,19 +615,30 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     // Excluded elements.
     $excluded_elements = $this->convertIncludedToExcludedPluginIds($this->elementManager, $form_state->getValue('excluded_elements'));
 
+    // Update config and submit form.
     $config = $this->config('webform.settings');
+
     $config->set('element', $form_state->getValue('element') +
       $form_state->getValue('checkbox') +
       $form_state->getValue('location') +
       $form_state->getValue('select') +
       ['excluded_elements' => $excluded_elements]
     );
+
     $config->set('html_editor', $form_state->getValue('html_editor'));
-    $config->set('file', $form_state->getValue('file'));
+
+    $file = $form_state->getValue('file');
+    $config->set('settings.default_form_file_limit', $file['default_form_file_limit']);
+    unset($file['default_form_file_limit']);
+    $config->set('file', $file);
+
     $config->set('format', $format);
-    $config->save();
 
     parent::submitForm($form, $form_state);
+
+    // Reset libraries cached.
+    // @see webform_library_info_build()
+    \Drupal::service('library.discovery')->clearCachedDefinitions();
   }
 
   /**
@@ -563,8 +654,13 @@ public static function validateExtensions($element, FormStateInterface $form_sta
    * Wrapper for FileItem::validateMaxFilesize.
    */
   public static function validateMaxFilesize($element, FormStateInterface $form_state) {
-    if (class_exists('\Drupal\file\Plugin\Field\FieldType\FileItem')) {
-      FileItem::validateMaxFilesize($element, $form_state);
+    // Issue #2359675: File field's Maximum upload size always passes validation.
+    // if (class_exists('\Drupal\file\Plugin\Field\FieldType\FileItem')) {
+    //   FileItem::validateMaxFilesize($element, $form_state);
+    // }
+    // @see \Drupal\file\Plugin\Field\FieldType\FileItem::validateMaxFilesize
+    if (!empty($element['#value']) && !Bytes::toInt($element['#value'])) {
+      $form_state->setError($element, t('The "@name" option must contain a valid value. You may either leave the text field empty or enter a string like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes).', ['@name' => $element['#title']]));
     }
   }
 
diff --git a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigExportersForm.php b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigExportersForm.php
index 7334c78b04bb477af68da4a129eec653cd1bc265..ac8c62679c2b499f1e5291f95173ba8a11269649 100644
--- a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigExportersForm.php
+++ b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigExportersForm.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Form\FormState;
+use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\webform\Plugin\WebformExporterManagerInterface;
 use Drupal\webform\WebformSubmissionExporterInterface;
@@ -14,6 +15,13 @@
  */
 class WebformAdminConfigExportersForm extends WebformAdminConfigBaseForm {
 
+  /**
+   * The file system service.
+   *
+   * @var \Drupal\Core\File\FileSystemInterface
+   */
+  protected $fileSystem;
+
   /**
    * The webform exporter manager.
    *
@@ -40,13 +48,16 @@ public function getFormId() {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\File\FileSystemInterface $file_system
+   *   The file system service.
    * @param \Drupal\webform\Plugin\WebformExporterManagerInterface $exporter_manager
    *   The webform exporter manager.
    * @param \Drupal\webform\WebformSubmissionExporterInterface $submission_exporter
    *   The webform submission exporter.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, WebformExporterManagerInterface $exporter_manager, WebformSubmissionExporterInterface $submission_exporter) {
+  public function __construct(ConfigFactoryInterface $config_factory, FileSystemInterface $file_system, WebformExporterManagerInterface $exporter_manager, WebformSubmissionExporterInterface $submission_exporter) {
     parent::__construct($config_factory);
+    $this->fileSystem = $file_system;
     $this->exporterManager = $exporter_manager;
     $this->submissionExporter = $submission_exporter;
   }
@@ -57,6 +68,7 @@ public function __construct(ConfigFactoryInterface $config_factory, WebformExpor
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('file_system'),
       $container->get('plugin.manager.webform.exporter'),
       $container->get('webform_submission.exporter')
     );
@@ -68,17 +80,30 @@ public static function create(ContainerInterface $container) {
   public function buildForm(array $form, FormStateInterface $form_state) {
     $config = $this->config('webform.settings');
 
-    // Export.
     $form['export_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Default export settings'),
+      '#title' => $this->t('Export general settings'),
+      '#open' => TRUE,
+    ];
+    $form['export_settings']['temp_directory'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Temporary directory'),
+      '#description' => $this->t('A local file system path where temporary export files will be stored. This directory should be persistent between requests and should not be accessible over the web.'),
+      '#required' => TRUE,
+      '#default_value' => $config->get('export.temp_directory') ?: file_directory_temp(),
+    ];
+
+    // Export.
+    $form['export_default_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Export default settings'),
       '#description' => $this->t('Enter default export settings to be used by all webforms.'),
       '#open' => TRUE,
     ];
 
     $export_options = $config->get('export');
     $export_form_state = new FormState();
-    $this->submissionExporter->buildExportOptionsForm($form['export_settings'], $export_form_state, $export_options);
+    $this->submissionExporter->buildExportOptionsForm($form['export_default_settings'], $export_form_state, $export_options);
 
     // (Excluded) Exporters.
     $form['exporter_types'] = [
@@ -95,16 +120,37 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     return parent::buildForm($form, $form_state);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    // Copied from: system_check_directory().
+    $temp_directory = $form_state->getValue('temp_directory');
+    if (!is_dir($temp_directory) && !$this->fileSystem->mkdir($temp_directory, NULL, TRUE)) {
+      $form_state->setErrorByName('temp_directory', $this->t('The directory %directory does not exist and could not be created.', ['%directory' => $temp_directory]));
+    }
+    if (is_dir($temp_directory) && !is_writable($temp_directory) && !$this->fileSystem->chmod($temp_directory)) {
+      $form_state->setErrorByName('temp_directory', $this->t('The directory %directory exists but is not writable and could not be made writable.', ['%directory' => $temp_directory]));
+    }
+    parent::validateForm($form, $form_state);
+  }
+
   /**
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
     $excluded_exporters = $this->convertIncludedToExcludedPluginIds($this->exporterManager, $form_state->getValue('excluded_exporters'));
 
-    $config = $this->config('webform.settings');
-    $config->set('export', $this->submissionExporter->getValuesFromInput($form_state->getValues()) + ['excluded_exporters' => $excluded_exporters]);
-    $config->save();
+    $values = $form_state->getValues();
+
+    $export = $this->submissionExporter->getValuesFromInput($values) + ['excluded_exporters' => $excluded_exporters];
 
+    // Set custom temp directory.
+    $export['temp_directory'] = ($values['temp_directory'] === file_directory_temp()) ? '' : $values['temp_directory'];
+
+    // Update config and submit form.
+    $config = $this->config('webform.settings');
+    $config->set('export', $export);
     parent::submitForm($form, $form_state);
   }
 
diff --git a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigFormsForm.php b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigFormsForm.php
index cde2800ca5f73b02e8510b6deb8856ff717a62f4..2f32fe256838c515545b5099efb4d5490663f5a4 100644
--- a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigFormsForm.php
+++ b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigFormsForm.php
@@ -3,10 +3,16 @@
 namespace Drupal\webform\Form\AdminConfig;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
+use Drupal\Core\Url;
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\webform\Element\WebformMessage;
 use Drupal\webform\Entity\Webform;
+use Drupal\webform\Utility\WebformArrayHelper;
 use Drupal\webform\WebformAddonsManagerInterface;
+use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformTokenManagerInterface;
 use Drupal\webform\WebformThirdPartySettingsManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -16,6 +22,13 @@
  */
 class WebformAdminConfigFormsForm extends WebformAdminConfigBaseForm {
 
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
   /**
    * The webform token manager.
    *
@@ -49,6 +62,8 @@ public function getFormId() {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
    * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
    *   The webform token manager.
    * @param \Drupal\webform\WebformThirdPartySettingsManagerInterface $third_party_settings_manager
@@ -56,8 +71,9 @@ public function getFormId() {
    * @param \Drupal\webform\WebformAddonsManagerInterface $addons_manager
    *   The webform add-ons manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, WebformTokenManagerInterface $token_manager, WebformThirdPartySettingsManagerInterface $third_party_settings_manager, WebformAddonsManagerInterface $addons_manager) {
+  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, WebformTokenManagerInterface $token_manager, WebformThirdPartySettingsManagerInterface $third_party_settings_manager, WebformAddonsManagerInterface $addons_manager) {
     parent::__construct($config_factory);
+    $this->moduleHandler = $module_handler;
     $this->tokenManager = $token_manager;
     $this->thirdPartySettingsManager = $third_party_settings_manager;
     $this->addonsManager = $addons_manager;
@@ -69,6 +85,7 @@ public function __construct(ConfigFactoryInterface $config_factory, WebformToken
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('module_handler'),
       $container->get('webform.token_manager'),
       $container->get('webform.third_party_settings_manager'),
       $container->get('webform.addons_manager')
@@ -92,7 +109,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $form['page_settings']['default_page_base_path'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Default base path for webform URLs'),
-      '#description' => $this->t('Leave blank to display the automatic generation of URL aliases for all webforms.'),
+      '#description' => $this->t('Leave blank to disable the automatic generation of URL aliases for all webforms.'),
       '#default_value' => $settings['default_page_base_path'],
     ];
     $form['page_settings']['default_page_base_path_message'] = [
@@ -109,10 +126,20 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     // Form settings.
     $form['form_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Form settings'),
+      '#title' => $this->t('Form general settings'),
       '#open' => TRUE,
       '#tree' => TRUE,
     ];
+    $form['form_settings']['default_status'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Default status'),
+      '#default_value' => $settings['default_status'],
+      '#options' => [
+        WebformInterface::STATUS_OPEN => $this->t('Open'),
+        WebformInterface::STATUS_CLOSED => $this->t('Closed'),
+      ],
+      '#options_display' => 'side_by_side',
+    ];
     $form['form_settings']['default_form_open_message'] = [
       '#type' => 'webform_html_editor',
       '#title' => $this->t('Default open message'),
@@ -135,10 +162,11 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#required' => TRUE,
       '#default_value' => $settings['default_form_confidential_message'],
     ];
-    $form['form_settings']['default_form_login_message'] = [
+    $form['form_settings']['default_form_access_denied_message'] = [
       '#type' => 'webform_html_editor',
-      '#title' => $this->t('Default login message when access denied to webform'),
-      '#default_value' => $settings['default_form_login_message'],
+      '#title' => $this->t('Default access denied message'),
+      '#required' => TRUE,
+      '#default_value' => $settings['default_form_access_denied_message'],
     ];
     $form['form_settings']['default_form_required_label'] = [
       '#type' => 'textfield',
@@ -173,7 +201,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#description' => $this->t('A list of classes that will be provided in "Button CSS classes" dropdown. Enter one or more classes on each line. These styles should be available in your theme\'s CSS file.'),
       '#default_value' => $settings['button_classes'],
     ];
-    $form['form_settings']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['form_settings']['token_tree_link'] = $this->tokenManager->buildTreeElement();
 
     // Form Behaviors.
     $form['form_behaviors'] = [
@@ -215,7 +243,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         'group' => $this->t('Validation'),
         'title' => $this->t('Disable inline form errors for all webforms'),
         'description' => $this->t('If checked, <a href=":href">inline form errors</a>  will be disabled for all webforms.', [':href' => 'https://www.drupal.org/docs/8/core/modules/inline-form-errors/inline-form-errors-module-overview']),
-        'access' => (\Drupal::moduleHandler()->moduleExists('inline_form_errors') && floatval(\Drupal::VERSION) >= 8.5),
+        'access' => \Drupal::moduleHandler()->moduleExists('inline_form_errors'),
       ],
       'default_form_required' => [
         'group' => $this->t('Validation'),
@@ -257,7 +285,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     // Wizard settings.
     $form['wizard_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Wizard settings'),
+      '#title' => $this->t('Form wizard settings'),
       '#open' => TRUE,
       '#tree' => TRUE,
     ];
@@ -293,7 +321,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     // Preview settings.
     $form['preview_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Preview settings'),
+      '#title' => $this->t('Form preview settings'),
       '#open' => TRUE,
       '#tree' => TRUE,
     ];
@@ -335,37 +363,12 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#description' => $this->t('A list of classes that will be provided in the "Preview CSS classes" dropdown. Enter one or more classes on each line. These styles should be available in your theme\'s CSS file.'),
       '#default_value' => $config->get('settings.preview_classes'),
     ];
-
-    // Draft settings.
-    $form['draft_settings'] = [
-      '#type' => 'details',
-      '#title' => $this->t('Draft settings'),
-      '#open' => TRUE,
-      '#tree' => TRUE,
-    ];
-    $form['draft_settings']['default_draft_button_label'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Default draft button label'),
-      '#required' => TRUE,
-      '#size' => 20,
-      '#default_value' => $settings['default_draft_button_label'],
-    ];
-    $form['draft_settings']['default_draft_saved_message'] = [
-      '#type' => 'webform_html_editor',
-      '#title' => $this->t('Default draft save message'),
-      '#default_value' => $settings['default_draft_saved_message'],
-    ];
-    $form['draft_settings']['default_draft_loaded_message'] = [
-      '#type' => 'webform_html_editor',
-      '#title' => $this->t('Default draft load message'),
-      '#default_value' => $settings['default_draft_loaded_message'],
-    ];
-    $form['draft_settings']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['preview_settings']['token_tree_link'] = $this->tokenManager->buildTreeElement();
 
     // Confirmation settings.
     $form['confirmation_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Confirmation settings'),
+      '#title' => $this->t('Form confirmation settings'),
       '#open' => TRUE,
       '#tree' => TRUE,
     ];
@@ -392,7 +395,126 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#description' => $this->t('A list of classes that will be provided in the "Confirmation back link CSS classes" dropdown. Enter one or more classes on each line. These styles should be available in your theme\'s CSS file.'),
       '#default_value' => $settings['confirmation_back_classes'],
     ];
-    $form['confirmation_settings']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['confirmation_settings']['token_tree_link'] = $this->tokenManager->buildTreeElement();
+
+    // Dialog settings.
+    $form['dialog_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Form dialog settings'),
+      '#open' => TRUE,
+      '#tree' => TRUE,
+    ];
+    $form['dialog_settings']['dialog_options'] = [
+      '#title' => $this->t('Dialog options'),
+      '#description' => [
+        '#markup' => $this->t('Enter preset dialog options available to all webforms.'),
+        'options' => [
+          '#theme' => 'item_list',
+          '#items' => [
+            $this->t('Name must be lower-case and contain only letters, numbers, and underscores.'),
+            $this->t('Width and height are optional.'),
+          ],
+        ],
+      ],
+      '#type' => 'webform_multiple',
+      '#key' => 'name',
+      '#header' => [
+        ['data' => $this->t('Machine name'), 'width' => '40%'],
+        ['data' => $this->t('Title'), 'width' => '40%'],
+        ['data' => $this->t('Width'), 'width' => '10%'],
+        ['data' => $this->t('Height'), 'width' => '10%'],
+      ],
+      '#element' => [
+        'name' => [
+          '#type' => 'textfield',
+          '#title' => $this->t('Dialog machine name'),
+          '#title_display' => 'invisible',
+          '#placeholder' => $this->t('Enter machine name…'),
+          '#pattern' => '^[a-z0-9_]*$',
+          '#error_no_message' => TRUE,
+        ],
+        'title' => [
+          '#type' => 'textfield',
+          '#title' => $this->t('Dialog title'),
+          '#placeholder' => $this->t('Enter title…'),
+          '#title_display' => 'invisible',
+          '#error_no_message' => TRUE,
+        ],
+        'width' => [
+          '#type' => 'number',
+          '#title' => $this->t('Dialog width'),
+          '#title_display' => 'invisible',
+          '#field_suffix' => 'px',
+          '#error_no_message' => TRUE,
+        ],
+        'height' => [
+          '#type' => 'number',
+          '#title' => $this->t('Dialog height'),
+          '#title_display' => 'invisible',
+          '#field_suffix' => 'px',
+          '#error_no_message' => TRUE,
+        ],
+      ],
+      '#error_no_message' => TRUE,
+      '#default_value' => $settings['dialog_options'],
+      '#parents' => ['dialog_settings', 'dialog_options'],
+    ];
+    $form['dialog_settings']['dialog'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable site-wide dialog support'),
+      '#description' => $this->t('If checked, the webform dialog library will be added to every page on your website, this allows any webform to be opened in a modal dialog.')
+        . '<br /><br />'
+        . $this->t('Webform specific dialog links will be included on all webform settings form.'),
+      '#return_value' => TRUE,
+      '#default_value' => $settings['dialog'],
+    ];
+    $form['dialog_settings']['dialog_messages'] = [
+      '#type' => 'container',
+      '#states' => [
+        'visible' => [
+          ':input[name="dialog_settings[dialog]"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    // Display warning when text formats do not support adding the class
+    // attribute to links.
+    if ($this->moduleHandler->moduleExists('filter')) {
+      /** @var \Drupal\filter\FilterFormatInterface[] $filter_formats */
+      $filter_formats = FilterFormat::loadMultiple();
+      $dialog_not_allowed = [];
+      foreach ($filter_formats as $filter_format) {
+        $html_restrictions = $filter_format->getHtmlRestrictions();
+        if ($html_restrictions && isset($html_restrictions['allowed']) && isset($html_restrictions['allowed']['a']) && !isset($html_restrictions['allowed']['a']['class'])) {
+          $dialog_not_allowed[] = $filter_format->label();
+        }
+      }
+      if ($dialog_not_allowed) {
+        $t_args = [
+          '@labels' => WebformArrayHelper::toString($dialog_not_allowed),
+          '@tag' => '<a href hreflang class>',
+          ':href' => Url::fromRoute('filter.admin_overview')->toString(),
+        ];
+        $form['dialog_settings']['dialog_messages']['filter_formats_message'] = [
+          '#type' => 'webform_message',
+          '#message_message' => $this->t('<strong>IMPORTANT:</strong> To insert dialog links using the @labels <a href=":href">text formats</a> the @tag must be added to the allowed HTML tags.', $t_args),
+          '#message_type' => 'warning',
+        ];
+      }
+    }
+    // Display install link module message.
+    if (!$this->moduleHandler->moduleExists('editor_advanced_link') && !$this->moduleHandler->moduleExists('menu_link_attributes')) {
+      $t_args = [
+        ':editor_advanced_link_href' => 'https://www.drupal.org/project/editor_advanced_link',
+        ':menu_link_attributes_href' => 'https://www.drupal.org/project/menu_link_attributes',
+      ];
+      $form['dialog_settings']['dialog_messages']['module_message'] = [
+        '#type' => 'webform_message',
+        '#message_message' => $this->t('To add the .webform-dialog class to a link\'s attributes, please use the <a href=":editor_advanced_link_href">D8 Editor Advanced link</a> or <a href=":menu_link_attributes_href">Menu Link Attributes</a> module.', $t_args),
+        '#message_type' => 'info',
+        '#message_close' => TRUE,
+        '#message_storage' => WebformMessage::STORAGE_SESSION,
+      ];
+    }
 
     // Third party settings.
     $form['third_party_settings'] = [
@@ -440,17 +562,23 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
       + $form_state->getValue('form_behaviors')
       + $form_state->getValue('wizard_settings')
       + $form_state->getValue('preview_settings')
-      + $form_state->getValue('draft_settings')
-      + $form_state->getValue('confirmation_settings');
+      + $form_state->getValue('confirmation_settings')
+      + $form_state->getValue('dialog_settings');
 
     // Track if we need to trigger an update of all webform paths
     // because the 'default_page_base_path' changed.
     $update_paths = ($settings['default_page_base_path'] != $this->config('webform.settings')->get('settings.default_page_base_path')) ? TRUE : FALSE;
 
+    // Filter empty dialog options.
+    foreach ($settings['dialog_options'] as $dialog_name => $dialog_options) {
+      $settings['dialog_options'][$dialog_name] = array_filter($dialog_options);
+    }
+
+    // Update config and submit form.
     $config = $this->config('webform.settings');
     $config->set('settings', $settings + $config->get('settings'));
     $config->set('third_party_settings', $form_state->getValue('third_party_settings') ?: []);
-    $config->save();
+    parent::submitForm($form, $form_state);
 
     /* Update paths */
 
@@ -461,8 +589,6 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
         $webform->updatePaths();
       }
     }
-
-    parent::submitForm($form, $form_state);
   }
 
 }
diff --git a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigHandlersForm.php b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigHandlersForm.php
index dc2939984c71f882b6929052a3dd321c6e9e6328..6e3a51a1c0cede72be1fec6c1f30f78899542888 100644
--- a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigHandlersForm.php
+++ b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigHandlersForm.php
@@ -3,6 +3,7 @@
 namespace Drupal\webform\Form\AdminConfig;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\webform\Plugin\WebformHandlerManagerInterface;
 use Drupal\webform\WebformTokenManagerInterface;
@@ -13,6 +14,13 @@
  */
 class WebformAdminConfigHandlersForm extends WebformAdminConfigBaseForm {
 
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
   /**
    * The webform token manager.
    *
@@ -39,13 +47,16 @@ public function getFormId() {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
    * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
    *   The webform token manager.
    * @param \Drupal\webform\Plugin\WebformHandlerManagerInterface $handler_manager
    *   The webform handler manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, WebformTokenManagerInterface $token_manager, WebformHandlerManagerInterface $handler_manager) {
+  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, WebformTokenManagerInterface $token_manager, WebformHandlerManagerInterface $handler_manager) {
     parent::__construct($config_factory);
+    $this->moduleHandler = $module_handler;
     $this->tokenManager = $token_manager;
     $this->handlerManager = $handler_manager;
   }
@@ -56,6 +67,7 @@ public function __construct(ConfigFactoryInterface $config_factory, WebformToken
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('module_handler'),
       $container->get('webform.token_manager'),
       $container->get('plugin.manager.webform.handler')
     );
@@ -77,7 +89,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
     $form['mail']['roles'] = [
       '#type' => 'webform_roles',
-      '#title' => $this->t('Recipent roles'),
+      '#title' => $this->t('Recipient roles'),
       '#description' => $this->t("Select roles that can be assigned to receive a webform's email. <em>Please note: Selected roles will be available to all webforms.</em>"),
       '#include_anonymous' => FALSE,
       '#default_value' => $config->get('mail.roles'),
@@ -148,7 +160,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#required' => TRUE,
       '#default_value' => $config->get('mail.default_body_html'),
     ];
-    $form['mail']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['mail']['token_tree_link'] = $this->tokenManager->buildTreeElement();
 
     // Email / Handler: Types.
     $form['handler_types'] = [
@@ -156,6 +168,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Submission handlers'),
       '#description' => $this->t('Select available submission handlers'),
       '#open' => TRUE,
+      '#weight' => 10,
     ];
     $form['handler_types']['excluded_handlers'] = $this->buildExcludedPlugins(
       $this->handlerManager,
@@ -188,11 +201,10 @@ public function buildForm(array $form, FormStateInterface $form_state) {
   public function submitForm(array &$form, FormStateInterface $form_state) {
     $excluded_handlers = $this->convertIncludedToExcludedPluginIds($this->handlerManager, $form_state->getValue('excluded_handlers'));
 
+    // Update config and submit form.
     $config = $this->config('webform.settings');
     $config->set('handler', ['excluded_handlers' => $excluded_handlers]);
     $config->set('mail', $form_state->getValue('mail'));
-    $config->save();
-
     parent::submitForm($form, $form_state);
   }
 
diff --git a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigLibrariesForm.php b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigLibrariesForm.php
index 542613192e82a4bdd1ba291567618b43593e8262..5751cf1154170374d885c05a4e4c6e8f2b432c88 100644
--- a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigLibrariesForm.php
+++ b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigLibrariesForm.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\webform\Plugin\WebformElement\TableSelect;
 use Drupal\webform\WebformLibrariesManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -90,32 +92,63 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#default_value' => $config->get('assets.javascript'),
     ];
 
-    // Libraries.
-    $form['libraries'] = [
+    // Libraries optional.
+    $form['libraries_optional'] = [
       '#type' => 'details',
-      '#title' => $this->t('External libraries'),
+      '#title' => $this->t('External optional libraries'),
       '#description' => $this->t('Uncheck the below optional external libraries that you do not want to be used by any webforms.') . '</br>' .
         '<em>' . $this->t('Please note, you can also exclude element types that are dependent on specific libraries.') . '</em>',
       '#open' => TRUE,
-      '#tree' => TRUE,
     ];
     $libraries_header = [
       'title' => ['data' => $this->t('Title')],
+      'version' => ['data' => $this->t('Version')],
       'description' => ['data' => $this->t('Description/Notes'), 'class' => [RESPONSIVE_PRIORITY_LOW]],
+      'elements' => ['data' => $this->t('Required elements'), 'class' => [RESPONSIVE_PRIORITY_LOW]],
+      'provider' => ['data' => $this->t('Provider'), 'class' => [RESPONSIVE_PRIORITY_LOW]],
+      'resources' => ['data' => $this->t('Resources'), 'class' => [RESPONSIVE_PRIORITY_LOW]],
     ];
 
     $this->libraries = [];
-    $libraries_options = [];
+    $libraries_optional_options = [];
+    $libraries_required_option = [];
     $libraries = $this->librariesManager->getLibraries();
     foreach ($libraries as $library_name => $library) {
-      // Only optional libraries can be excluded.
-      if (empty($library['optional'])) {
-        continue;
+      $operations = [];
+      $operations['homepage'] = [
+        'title' => $this->t('Homepage'),
+        'url' => $library['homepage_url'],
+      ];
+      if (isset($library['download_url'])) {
+        $operations['download'] = [
+          'title' => $this->t('Download'),
+          'url' => $library['download_url'],
+        ];
+      }
+      if (isset($library['issues_url'])) {
+        $issues_url = $library['issues_url'];
+      }
+      elseif (isset($library['download_url']) && preg_match('#https://github.com/[^/]+/[^/]+#', $library['download_url']->toString(), $match)) {
+        $issues_url = Url::fromUri($match[0] . '/issues');
+      }
+      else {
+        $issues_url = NULL;
+      }
+      if ($issues_url) {
+        $operations['issues'] = [
+          'title' => $this->t('Open Issues'),
+          'url' => $issues_url,
+        ];
+        $accessibility_url = clone $issues_url;
+        $operations['accessibility'] = [
+          'title' => $this->t('Accessibility Issues'),
+          'url' => $accessibility_url->setOption('query', ['q' => 'is:issue is:open accessibility ']),
+        ];
       }
 
-      $this->libraries[$library_name] = $library_name;
-      $libraries_options[$library_name] = [
+      $library_option = [
         'title' => $library['title'],
+        'version' => $library['version'],
         'description' => [
           'data' => [
             'content' => ['#markup' => $library['description'], '#suffix' => '<br />'],
@@ -127,33 +160,70 @@ public function buildForm(array $form, FormStateInterface $form_state) {
             ] : [],
           ],
         ],
+        'elements' => ['data' => ['#markup' => (isset($library['elements'])) ? implode('<br/>', $library['elements']) : '']],
+        'provider' => $library['provider'],
+        'resources' => [
+          'data' => [
+            '#type' => 'operations',
+            '#links' => $operations,
+            '#prefix' => '<div class="webform-dropbutton">',
+            '#suffix' => '</div>',
+          ],
+        ],
       ];
+
+      // Only optional libraries can be excluded.
+      if (empty($library['optional'])) {
+        $libraries_required_options[$library_name] = $library_option;
+      }
+      else {
+        $this->libraries[$library_name] = $library_name;
+        $libraries_optional_options[$library_name] = $library_option;
+      }
     }
 
-    $form['libraries']['excluded_libraries'] = [
+    $form['libraries_optional']['excluded_libraries'] = [
       '#type' => 'tableselect',
       '#title' => $this->t('Libraries'),
       '#header' => $libraries_header,
       '#js_select' => FALSE,
-      '#options' => $libraries_options,
+      '#options' => $libraries_optional_options,
       '#default_value' => array_diff($this->libraries, array_combine($config->get('libraries.excluded_libraries'), $config->get('libraries.excluded_libraries'))),
     ];
+    TableSelect::setProcessTableSelectCallback($form['libraries_optional']['excluded_libraries']);
+
+    // Display warning message when select2 and chosen are enabled.
     $t_args = [
       ':select2_href' => $libraries['jquery.select2']['homepage_url']->toString(),
       ':chosen_href' => $libraries['jquery.chosen']['homepage_url']->toString(),
     ];
-    $form['libraries']['select_message'] = [
+    $form['libraries_optional']['select_message'] = [
       '#type' => 'webform_message',
       '#message_type' => 'warning',
       '#message_message' => $this->t('<a href=":select2_href">Select2</a> and <a href=":chosen_href">Chosen</a> provide very similar functionality, most websites should only have one of these libraries enabled.', $t_args),
       '#states' => [
         'visible' => [
-          ':input[name="libraries[excluded_libraries][jquery.select2]"]' => ['checked' => TRUE],
-          ':input[name="libraries[excluded_libraries][jquery.chosen]"]' => ['checked' => TRUE],
+          ':input[name="excluded_libraries[jquery.select2]"]' => ['checked' => TRUE],
+          ':input[name="excluded_libraries[jquery.chosen]"]' => ['checked' => TRUE],
         ],
       ],
     ];
 
+    // Libraries required.
+    if ($libraries_required_option) {
+      $form['libraries_required'] = [
+        '#type' => 'details',
+        '#title' => $this->t('External required libraries'),
+        '#description' => $this->t('The below external libraries are required by specified webform elements or modules.'),
+        '#open' => TRUE,
+      ];
+      $form['libraries_required']['required_libraries'] = [
+        '#type' => 'table',
+        '#header' => $libraries_header,
+        '#rows' => $libraries_required_options,
+      ];
+    }
+
     return parent::buildForm($form, $form_state);
   }
 
@@ -162,25 +232,23 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
     // Convert list of included types to excluded types.
-    $libraries = $form_state->getValue('libraries');
-    $libraries['excluded_libraries'] = array_diff($this->libraries, array_filter($libraries['excluded_libraries']));
-    ksort($libraries['excluded_libraries']);
+    $excluded_libraries = array_diff($this->libraries, array_filter($form_state->getValue('excluded_libraries')));
+    ksort($excluded_libraries);
 
     // Note: Must store a simple array of libraries because library names
     // may contain periods, which is not supported by Drupal's
     // config management.
-    $libraries['excluded_libraries'] = array_keys($libraries['excluded_libraries']);
+    $excluded_libraries = array_keys($excluded_libraries);
 
+    // Update config and submit form.
     $config = $this->config('webform.settings');
     $config->set('assets', $form_state->getValue('assets'));
-    $config->set('libraries', $libraries);
-    $config->save();
+    $config->set('libraries.excluded_libraries', $excluded_libraries);
+    parent::submitForm($form, $form_state);
 
     // Reset libraries cached.
     // @see webform_library_info_build()
     \Drupal::service('library.discovery')->clearCachedDefinitions();
-
-    parent::submitForm($form, $form_state);
   }
 
 }
diff --git a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigSubmissionsForm.php b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigSubmissionsForm.php
index 765364459154c543d7ea05a4f60ceed0c73cf196..65c79a31c414729e9d42af743d3e4114f4586239 100644
--- a/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigSubmissionsForm.php
+++ b/web/modules/webform/src/Form/AdminConfig/WebformAdminConfigSubmissionsForm.php
@@ -3,7 +3,9 @@
 namespace Drupal\webform\Form\AdminConfig;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Element\WebformMessage;
 use Drupal\webform\WebformTokenManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -12,6 +14,13 @@
  */
 class WebformAdminConfigSubmissionsForm extends WebformAdminConfigBaseForm {
 
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
   /**
    * The webform token manager.
    *
@@ -31,11 +40,14 @@ public function getFormId() {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
    * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
    *   The webform token manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, WebformTokenManagerInterface $token_manager) {
+  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, WebformTokenManagerInterface $token_manager) {
     parent::__construct($config_factory);
+    $this->moduleHandler = $module_handler;
     $this->tokenManager = $token_manager;
   }
 
@@ -45,6 +57,7 @@ public function __construct(ConfigFactoryInterface $config_factory, WebformToken
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('module_handler'),
       $container->get('webform.token_manager')
     );
   }
@@ -59,15 +72,15 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     // Submission settings.
     $form['submission_settings'] = [
       '#type' => 'details',
-      '#title' => $this->t('Submission settings'),
+      '#title' => $this->t('Submission general settings'),
       '#open' => TRUE,
       '#tree' => TRUE,
     ];
-    $form['submission_settings']['default_submission_login_message'] = [
+    $form['submission_settings']['default_submission_access_denied_message'] = [
       '#type' => 'webform_html_editor',
-      '#title' => $this->t('Default login message when access denied to submission'),
+      '#title' => $this->t('Default access denied message'),
       '#required' => TRUE,
-      '#default_value' => $settings['default_submission_login_message'],
+      '#default_value' => $settings['default_submission_access_denied_message'],
     ];
     $form['submission_settings']['default_submission_exception_message'] = [
       '#type' => 'webform_html_editor',
@@ -81,6 +94,18 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#required' => TRUE,
       '#default_value' => $settings['default_submission_locked_message'],
     ];
+    $form['submission_settings']['default_previous_submission_message'] = [
+      '#type' => 'webform_html_editor',
+      '#title' => $this->t('Default previous submission message'),
+      '#required' => TRUE,
+      '#default_value' => $settings['default_previous_submission_message'],
+    ];
+    $form['submission_settings']['default_previous_submissions_message'] = [
+      '#type' => 'webform_html_editor',
+      '#title' => $this->t('Default previous submissions message'),
+      '#required' => TRUE,
+      '#default_value' => $settings['default_previous_submissions_message'],
+    ];
     $form['submission_settings']['default_autofill_message'] = [
       '#type' => 'webform_html_editor',
       '#title' => $this->t('Default autofill message'),
@@ -93,7 +118,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#required' => TRUE,
       '#default_value' => $settings['default_submission_label'],
     ];
-    $form['submission_settings']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['submission_settings']['token_tree_link'] = $this->tokenManager->buildTreeElement();
 
     // Submission Behaviors.
     $form['submission_behaviors'] = [
@@ -105,7 +130,8 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $behavior_elements = [
       'default_submission_log' => [
         'title' => $this->t('Log all submission events for all webforms'),
-        'description' => $this->t('If checked, all submission events will be logged to dedicated submission log available to all webforms and submissions.'),
+        'description' => $this->t('If checked, all submission events will be logged to dedicated submission log available to all webforms and submissions.') . '<br/><br/>' .
+          '<em>' . t('The webform submission log will track more detailed user information including email addresses and subjects.') . '</em>',
       ],
     ];
     foreach ($behavior_elements as $behavior_key => $behavior_element) {
@@ -117,11 +143,25 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         '#default_value' => $settings[$behavior_key],
       ];
     }
+    if (!$this->moduleHandler->moduleExists('webform_submission_log')) {
+      $form['submission_behaviors']['webform_submission_log_message'] = [
+        '#type' => 'webform_message',
+        '#message_type' => 'info',
+        '#message_message' => $this->t("Enable the 'Webform Submission Log' module to better track and permanently store submission logs."),
+        '#message_close' => TRUE,
+        '#message_storage' => WebformMessage::STORAGE_SESSION,
+        '#states' => [
+          'visible' => [
+            ':input[name="submission_behaviors[default_submission_log]"]' => ['checked' => TRUE],
+          ],
+        ],
+      ];
+    }
 
     // Submission limits.
     $form['submission_limits'] = [
       '#type' => 'details',
-      '#title' => $this->t('Submission limits'),
+      '#title' => $this->t('Submission limit settings'),
       '#open' => TRUE,
       '#tree' => TRUE,
     ];
@@ -135,12 +175,38 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Default per user submission limit message'),
       '#default_value' => $config->get('settings.default_limit_user_message'),
     ];
-    $form['submission_limits']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['submission_limits']['token_tree_link'] = $this->tokenManager->buildTreeElement();
+
+    // Draft settings.
+    $form['draft_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Submission draft settings'),
+      '#open' => TRUE,
+      '#tree' => TRUE,
+    ];
+    $form['draft_settings']['default_draft_button_label'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Default draft button label'),
+      '#required' => TRUE,
+      '#size' => 20,
+      '#default_value' => $settings['default_draft_button_label'],
+    ];
+    $form['draft_settings']['default_draft_saved_message'] = [
+      '#type' => 'webform_html_editor',
+      '#title' => $this->t('Default draft save message'),
+      '#default_value' => $settings['default_draft_saved_message'],
+    ];
+    $form['draft_settings']['default_draft_loaded_message'] = [
+      '#type' => 'webform_html_editor',
+      '#title' => $this->t('Default draft load message'),
+      '#default_value' => $settings['default_draft_loaded_message'],
+    ];
+    $form['draft_settings']['token_tree_link'] = $this->tokenManager->buildTreeElement();
 
     // Submission purging.
     $form['purge'] = [
       '#type' => 'details',
-      '#title' => $this->t('Submission purging'),
+      '#title' => $this->t('Submission purge settings'),
       '#open' => TRUE,
       '#tree' => TRUE,
     ];
@@ -152,6 +218,33 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#description' => $this->t('Enter the amount of submissions to be purged during single cron run. You may want to lower this number if you are facing memory or timeout issues when purging via cron.'),
     ];
 
+    // Submission views.
+    $form['views_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Submission views settings'),
+      '#open' => TRUE,
+      '#tree' => TRUE,
+    ];
+    $form['views_settings']['default_submission_views'] = [
+      '#type' => 'webform_submission_views',
+      '#title' => $this->t('Submission views'),
+      '#title_display' => 'invisible',
+      '#global' => TRUE,
+      '#default_value' => $settings['default_submission_views'],
+    ];
+    $form['views_settings']['message'] = [
+      '#type' => 'webform_message',
+      '#message_type' => 'info',
+      '#message_message' => $this->t('Uncheck the below settings to allow webform administrators to choose which results should be replaced with submission views.'),
+      '#message_close' => TRUE,
+      '#message_storage' => WebformMessage::STORAGE_SESSION,
+    ];
+    $form['views_settings']['default_submission_views_replace'] = [
+      '#type' => 'webform_submission_views_replace',
+      '#global' => TRUE,
+      '#default_value' => $settings['default_submission_views_replace'],
+    ];
+
     $this->tokenManager->elementValidate($form);
 
     return parent::buildForm($form, $form_state);
@@ -163,13 +256,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
   public function submitForm(array &$form, FormStateInterface $form_state) {
     $settings = $form_state->getValue('submission_settings')
       + $form_state->getValue('submission_behaviors')
-      + $form_state->getValue('submission_limits');
+      + $form_state->getValue('submission_limits')
+      + $form_state->getValue('draft_settings')
+      + $form_state->getValue('views_settings');
 
+    // Update config and submit form.
     $config = $this->config('webform.settings');
     $config->set('settings', $settings + $config->get('settings'));
     $config->set('purge', $form_state->getValue('purge'));
-    $config->save();
-
     parent::submitForm($form, $form_state);
   }
 
diff --git a/web/modules/webform/src/Form/WebformAjaxFormTrait.php b/web/modules/webform/src/Form/WebformAjaxFormTrait.php
index 0129e9354e886bbb89322977bcf09a3990993261..cfd42b2866aabc3ed7ef33ed12b9ea65e0679e85 100644
--- a/web/modules/webform/src/Form/WebformAjaxFormTrait.php
+++ b/web/modules/webform/src/Form/WebformAjaxFormTrait.php
@@ -8,10 +8,12 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Url;
+use Drupal\webform\Ajax\WebformAnnounceCommand;
 use Drupal\webform\Ajax\WebformCloseDialogCommand;
 use Drupal\webform\Ajax\WebformRefreshCommand;
 use Drupal\webform\Ajax\WebformScrollTopCommand;
 use Drupal\webform\Ajax\WebformSubmissionAjaxResponse;
+use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\WebformSubmissionForm;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 
@@ -46,7 +48,7 @@ abstract public function cancelAjaxForm(array &$form, FormStateInterface $form_s
    * Get default ajax callback settings.
    *
    * @return array
-   *   An associative array containing  default ajax callback settings.
+   *   An associative array containing default ajax callback settings.
    */
   protected function getDefaultAjaxSettings() {
     return [
@@ -122,25 +124,28 @@ protected function buildAjaxForm(array &$form, FormStateInterface $form_state, a
     // Apply default settings.
     $settings += $this->getDefaultAjaxSettings();
 
-    // Make sure the form has (submit) actions.
-    if (!isset($form['actions'])) {
-      return $form;
-    }
-
     // Add Ajax callback to all submit buttons.
-    foreach (Element::children($form['actions']) as $key) {
-      $is_submit_button = (isset($form['actions'][$key]['#type']) && $form['actions'][$key]['#type'] == 'submit');
-      if ($is_submit_button) {
-        $form['actions'][$key]['#ajax'] = [
-          'callback' => '::submitAjaxForm',
-          'event' => 'click',
-        ] + $settings;
+    foreach (Element::children($form) as $element_key) {
+      if (!WebformElementHelper::isType($form[$element_key], 'actions')) {
+        continue;
+      }
+
+      $actions =& $form[$element_key];
+      foreach (Element::children($actions) as $action_key) {
+        if (WebformElementHelper::isType($actions[$action_key], 'submit')) {
+          $actions[$action_key]['#ajax'] = [
+            'callback' => '::submitAjaxForm',
+            'event' => 'click',
+          ] + $settings;
+        }
       }
     }
 
-    // Add Ajax wrapper around the form.
-    $form['#form_wrapper_id'] = $this->getWrapperId();
-    $form['#prefix'] = '<div id="' . $this->getWrapperId() . '">';
+    // Add Ajax wrapper with wrapper content bookmark around the form.
+    // @see Drupal.AjaxCommands.prototype.webformScrollTop
+    $wrapper_id = $this->getWrapperId();
+    $form['#form_wrapper_id'] = $wrapper_id;
+    $form['#prefix'] = '<a id="' . $wrapper_id . '-content" tabindex="-1"></a><div id="' . $wrapper_id . '">';
     $form['#suffix'] = '</div>';
 
     // Add Ajax library which contains 'Scroll to top' Ajax command and
@@ -164,13 +169,16 @@ protected function buildAjaxForm(array &$form, FormStateInterface $form_state, a
    */
   public function submitAjaxForm(array &$form, FormStateInterface $form_state) {
     $scroll_top_target = (isset($form['#webform_ajax_scroll_top'])) ? $form['#webform_ajax_scroll_top'] : 'form';
+
     if ($form_state->hasAnyErrors()) {
       // Display validation errors and scroll to the top of the page.
       $response = $this->replaceForm($form, $form_state);
       if ($scroll_top_target) {
         $response->addCommand(new WebformScrollTopCommand('#' . $this->getWrapperId(), $scroll_top_target));
       }
-      return $response;
+
+      // Announce validation errors.
+      $this->announce($this->t('Form validation errors have been found.'));
     }
     elseif ($form_state->isRebuilding()) {
       // Rebuild form.
@@ -178,18 +186,26 @@ public function submitAjaxForm(array &$form, FormStateInterface $form_state) {
       if ($scroll_top_target) {
         $response->addCommand(new WebformScrollTopCommand('#' . $this->getWrapperId(), $scroll_top_target));
       }
-      return $response;
     }
     elseif ($redirect_url = $this->getFormStateRedirectUrl($form_state)) {
       // Redirect to URL.
       $response = $this->createAjaxResponse($form, $form_state);
       $response->addCommand(new WebformCloseDialogCommand());
       $response->addCommand(new WebformRefreshCommand($redirect_url));
-      return $response;
     }
     else {
-      return $this->cancelAjaxForm($form, $form_state);
+      $response = $this->cancelAjaxForm($form, $form_state);
     }
+
+    // Add announcements to Ajax response and then reset the announcements.
+    // @see \Drupal\webform\Form\WebformAjaxFormTrait::announce
+    $announcements = $this->getAnnouncements();
+    foreach ($announcements as $announcement) {
+      $response->addCommand(new WebformAnnounceCommand($announcement['text'], $announcement['priority']));
+    }
+    $this->resetAnnouncements();
+
+    return $response;
   }
 
   /**
@@ -268,7 +284,7 @@ protected function replaceForm(array $form, FormStateInterface $form_state) {
    */
   protected function getFormStateRedirectUrl(FormStateInterface $form_state) {
     // Always check the ?destination which is used by the off-canvas/system tray.
-    if (\Drupal::request()->get('destination')) {
+    if ($this->getRequest()->get('destination')) {
       $destination = $this->getRedirectDestination()->get();
       return (strpos($destination, $destination) === 0) ? $destination : base_path() . $destination;
     }
@@ -280,7 +296,7 @@ protected function getFormStateRedirectUrl(FormStateInterface $form_state) {
     // Re-enable redirect, grab the URL, and then disable again.
     $no_redirect = $form_state->isRedirectDisabled();
     $form_state->disableRedirect(FALSE);
-    $redirect = $form_state->getRedirect() ?: $form_state->getResponse();
+    $redirect = $form_state->getResponse() ?: $form_state->getRedirect();
     $form_state->disableRedirect($no_redirect);
 
     if ($redirect instanceof RedirectResponse) {
@@ -294,4 +310,66 @@ protected function getFormStateRedirectUrl(FormStateInterface $form_state) {
     }
   }
 
+  /****************************************************************************/
+  // Drupal.announce handling.
+  //
+  // Announcements are stored in the user session because the $form_state
+  // is already serialized (and can't be altered) when announcements
+  // are added to Ajax response.
+  // @see \Drupal\webform\Form\WebformAjaxFormTrait::submitAjaxForm
+  /****************************************************************************/
+
+  /**
+   * Queue announcement with Ajax response.
+   *
+   * @param string $text
+   *   A string to be read by the UA.
+   * @param string $priority
+   *   A string to indicate the priority of the message. Can be either
+   *   'polite' or 'assertive'.
+   *
+   * @see \Drupal\webform\Ajax\WebformAnnounceCommand
+   * @see \Drupal\webform\Form\WebformAjaxFormTrait::submitAjaxForm
+   */
+  protected function announce($text, $priority = 'polite') {
+    $announcements = $this->getAnnouncements();
+    $announcements[] = [
+      'text' => $text,
+      'priority' => $priority,
+    ];
+    $this->setAnnouncements($announcements);
+  }
+
+  /**
+   * Get announcements.
+   *
+   * @return array
+   *   An associative array of announcements.
+   */
+  protected function getAnnouncements() {
+    $session = $this->getRequest()->getSession();
+    return $session->get('announcements') ?: [];
+  }
+
+  /**
+   * Set announcements.
+   *
+   * @param array $announcements
+   *   An associative array of announcements.
+   */
+  protected function setAnnouncements(array $announcements) {
+    $session = $this->getRequest()->getSession();
+    $session->set('announcements', $announcements);
+    $session->save();
+  }
+
+  /**
+   * Reset announcements.
+   */
+  protected function resetAnnouncements() {
+    $session = $this->getRequest()->getSession();
+    $session->remove('announcements');
+    $session->save();
+  }
+
 }
diff --git a/web/modules/webform/src/Form/WebformConfigEntityDeleteFormBase.php b/web/modules/webform/src/Form/WebformConfigEntityDeleteFormBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..6393b38a6a5471b86ec6064d5475c61ed9489f70
--- /dev/null
+++ b/web/modules/webform/src/Form/WebformConfigEntityDeleteFormBase.php
@@ -0,0 +1,199 @@
+<?php
+
+namespace Drupal\webform\Form;
+
+use Drupal\Core\Entity\EntityDeleteFormTrait;
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Form\ConfirmFormHelper;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a generic base class for a webform entity deletion form.
+ *
+ * Copied from: \Drupal\Core\Entity\EntityConfirmFormBase.
+ */
+abstract class WebformConfigEntityDeleteFormBase extends EntityForm implements WebformDeleteFormInterface {
+
+  use EntityDeleteFormTrait;
+  use WebformDialogFormTrait;
+
+  /**
+   * Display confirmation checkbox.
+   *
+   * @var bool
+   */
+  protected $confirmCheckbox = TRUE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBaseFormId() {
+    return $this->entity->getEntityTypeId() . '_confirm_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    if ($this->isDialog()) {
+      $t_args = [
+        '@entity-type' => $this->getEntity()->getEntityType()->getLowercaseLabel(),
+        '@label' => $this->getEntity()->label(),
+      ];
+      return $this->t("Delete '@label' @entity-type?", $t_args);
+    }
+    else {
+      $t_args = [
+        '@entity-type' => $this->getEntity()->getEntityType()->getLowercaseLabel(),
+        '%label' => $this->getEntity()->label(),
+      ];
+      return $this->t('Delete %label @entity-type?', $t_args);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWarning() {
+    $t_args = [
+      '@entity-type' => $this->getEntity()->getEntityType()->getLowercaseLabel(),
+      '%label' => $this->getEntity()->label(),
+    ];
+
+    return [
+      '#type' => 'webform_message',
+      '#message_type' => 'warning',
+      '#message_message' => $this->t('Are you sure you want to delete the %label @entity-type?', $t_args) . '<br/>' .
+        '<strong>' . $this->t('This action cannot be undone.') . '</strong>',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDetails() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmInput() {
+    $t_args = [
+      '@entity-type' => $this->getEntity()->getEntityType()->getLowercaseLabel(),
+      '%label' => $this->getEntity()->label(),
+    ];
+
+    if ($this->confirmCheckbox) {
+      return [
+        '#type' => 'checkbox',
+        '#title' => $this->t('Yes, I want to delete the %label @entity-type', $t_args),
+        '#required' => TRUE,
+      ];
+    }
+    else {
+      return [];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelText() {
+    return $this->t('Cancel');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormName() {
+    return 'webform_config_entity_delete';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildForm($form, $form_state);
+    $form['#attributes']['class'][] = 'confirmation';
+    $form['#theme'] = 'confirm_form';
+    $form[$this->getFormName()] = ['#type' => 'hidden', '#value' => 1];
+
+    // Title.
+    $form['#title'] = $this->getQuestion();
+
+    // Warning.
+    $form['warning'] = $this->getWarning();
+
+    // Description.
+    $form['description'] = $this->getDescription();
+
+    // Details and confirm input.
+    $details = $this->getDetails();
+    $confirm_input = $this->getConfirmInput();
+    if ($details) {
+      $form['details'] = $details;
+    }
+    if (!$details && $confirm_input) {
+      $form['hr'] = ['#markup' => '<p><hr/></p>'];
+    }
+    if ($confirm_input) {
+      $form['confirm'] = $confirm_input;
+    }
+
+    // Dialog.
+    return $this->buildDialogConfirmForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, FormStateInterface $form_state) {
+    return [
+      'submit' => [
+        '#type' => 'submit',
+        '#value' => $this->getConfirmText(),
+        '#submit' => [
+          [$this, 'submitForm'],
+        ],
+      ],
+      'cancel' => ConfirmFormHelper::buildCancelLink($this, $this->getRequest()),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * The save() method is not used in EntityConfirmFormBase. This overrides the
+   * default implementation that saves the entity.
+   *
+   * Confirmation forms should override submitForm() instead for their logic.
+   */
+  public function save(array $form, FormStateInterface $form_state) {}
+
+  /**
+   * {@inheritdoc}
+   *
+   * The delete() method is not used in EntityConfirmFormBase. This overrides
+   * the default implementation that redirects to the delete-form confirmation
+   * form.
+   *
+   * Confirmation forms should override submitForm() instead for their logic.
+   */
+  public function delete(array $form, FormStateInterface $form_state) {}
+
+}
diff --git a/web/modules/webform/src/Form/WebformContributeForm.php b/web/modules/webform/src/Form/WebformContributeForm.php
index 55d91570c98edcaffb9c0edb2f5bb45ea19ea474..61bb605fedd41058a8318fc8575ef41b687ca978 100644
--- a/web/modules/webform/src/Form/WebformContributeForm.php
+++ b/web/modules/webform/src/Form/WebformContributeForm.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\webform\Form;
 
-use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\HtmlCommand;
 use Drupal\Core\Ajax\RedirectCommand;
@@ -11,7 +10,6 @@
 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteBuilderInterface;
 use Drupal\Core\Url;
 use Drupal\webform\WebformContributeManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -21,20 +19,6 @@
  */
 class WebformContributeForm extends ConfigFormBase {
 
-  /**
-   * The render cache bin.
-   *
-   * @var \Drupal\Core\Cache\CacheBackendInterface
-   */
-  protected $renderCache;
-
-  /**
-   * The router builder.
-   *
-   * @var \Drupal\Core\Routing\RouteBuilderInterface
-   */
-  protected $routerBuilder;
-
   /**
    * The contribute manager.
    *
@@ -57,21 +41,15 @@ public function getEditableConfigNames() {
   }
 
   /**
-   * Constructs a ContributeSettingsForm object.
+   * Constructs a WebformContributeForm object.
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
-   * @param \Drupal\Core\Cache\CacheBackendInterface $render_cache
-   *   The render cache service.
-   * @param \Drupal\Core\Routing\RouteBuilderInterface $router_builder
-   *   The router builder service.
    * @param \Drupal\webform\WebformContributeManagerInterface $contribute_manager
    *   The contribute manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, CacheBackendInterface $render_cache, RouteBuilderInterface $router_builder, WebformContributeManagerInterface $contribute_manager) {
+  public function __construct(ConfigFactoryInterface $config_factory, WebformContributeManagerInterface $contribute_manager) {
     parent::__construct($config_factory);
-    $this->renderCache = $render_cache;
-    $this->routerBuilder = $router_builder;
     $this->contributeManager = $contribute_manager;
   }
 
@@ -81,8 +59,6 @@ public function __construct(ConfigFactoryInterface $config_factory, CacheBackend
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
-      $container->get('cache.render'),
-      $container->get('router.builder'),
       $container->get('webform.contribute_manager')
     );
   }
@@ -213,12 +189,12 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     if ((string) $form_state->getValue('op') === (string) $this->t('Clear')) {
       $account_type = NULL;
       $account_id = NULL;
-      drupal_set_message($this->t('Community information has been cleared.'));
+      $this->messenger()->addStatus($this->t('Community information has been cleared.'));
     }
     else {
       $account_type = $form_state->getValue('account_type');
       $account_id = $form_state->getValue($account_type . '_id');
-      drupal_set_message($this->t('Your community information has been saved.'));
+      $this->messenger()->addStatus($this->t('Your community information has been saved.'));
     }
 
     // Always clear cached information.
diff --git a/web/modules/webform/src/Form/WebformDeleteFormBase.php b/web/modules/webform/src/Form/WebformDeleteFormBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..c4f36934c4df60dec56b7fad9083acc206f5ba1c
--- /dev/null
+++ b/web/modules/webform/src/Form/WebformDeleteFormBase.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Drupal\webform\Form;
+
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\ConfirmFormHelper;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a generic base class for a webform deletion form.
+ */
+abstract class WebformDeleteFormBase extends ConfirmFormBase implements WebformDeleteFormInterface {
+
+  use WebformDialogFormTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormName() {
+    return 'webform_delete';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['#attributes']['class'][] = 'confirmation';
+    $form['#theme'] = 'confirm_form';
+    $form[$this->getFormName()] = ['#type' => 'hidden', '#value' => 1];
+
+    // Title.
+    $form['#title'] = $this->getQuestion();
+
+    // Warning.
+    $form['warning'] = $this->getWarning();
+
+    // Description.
+    $form['description'] = $this->getDescription();
+
+    // Details and confirm input.
+    $details = $this->getDetails();
+    $confirm_input = $this->getConfirmInput();
+    if ($details) {
+      $form['details'] = $details;
+    }
+    if (!$details && $confirm_input) {
+      $form['hr'] = ['#markup' => '<p><hr/></p>'];
+    }
+    if ($confirm_input) {
+      $form['confirm'] = $confirm_input;
+    }
+
+    // Actions.
+    $form['actions'] = ['#type' => 'actions'];
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->getConfirmText(),
+      '#button_type' => 'primary',
+    ];
+    $form['actions']['cancel'] = ConfirmFormHelper::buildCancelLink($this, $this->getRequest());
+
+    return $this->buildDialogConfirmForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWarning() {
+    return [
+      '#type' => 'webform_message',
+      '#message_type' => 'warning',
+      '#message_message' => $this->t('Are you sure you want to delete this?') . '<br/>' .
+        '<strong>' . $this->t('This action cannot be undone.') . '</strong>',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDetails() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmInput() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Delete');
+  }
+
+}
diff --git a/web/modules/webform/src/Form/WebformDeleteFormInterface.php b/web/modules/webform/src/Form/WebformDeleteFormInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..6e7b7b42be0ea673dbe004e3951d46787125ef02
--- /dev/null
+++ b/web/modules/webform/src/Form/WebformDeleteFormInterface.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\webform\Form;
+
+use Drupal\Core\Form\ConfirmFormInterface;
+
+/**
+ * Defines an interface for webform delete forms.
+ */
+interface WebformDeleteFormInterface extends ConfirmFormInterface {
+
+  /**
+   * Returns warning message to display.
+   *
+   * @return array
+   *   A renderable array containing a warning message.
+   */
+  public function getWarning();
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription();
+
+  /**
+   * Returns details to display.
+   *
+   * @return array
+   *   A renderable array containing details.
+   */
+  public function getDetails();
+
+  /**
+   * Returns confirm input to display.
+   *
+   * @return array
+   *   A renderable array containing confirm input.
+   */
+  public function getConfirmInput();
+
+}
diff --git a/web/modules/webform/src/Form/WebformDialogFormTrait.php b/web/modules/webform/src/Form/WebformDialogFormTrait.php
index 4e41a781cf0e108bfde3e15d5b89b6d0fb98b607..994926e0e6bf4a23d2d6141a52d6d396d9e45a39 100644
--- a/web/modules/webform/src/Form/WebformDialogFormTrait.php
+++ b/web/modules/webform/src/Form/WebformDialogFormTrait.php
@@ -5,6 +5,8 @@
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\CloseDialogCommand;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\webform\Utility\WebformDialogHelper;
 
 /**
  * Trait class for Webform Ajax dialog support.
@@ -59,8 +61,8 @@ protected function buildDialogConfirmForm(array &$form, FormStateInterface $form
     $form['actions']['cancel'] = [
       '#type' => 'submit',
       '#value' => $this->t('Cancel'),
+      '#validate' => ['::noValidate'],
       '#submit' => ['::noSubmit'],
-      '#validate' => ['::noSubmit'],
       '#weight' => 100,
       '#ajax' => [
         'callback' => '::cancelAjaxForm',
@@ -70,6 +72,46 @@ protected function buildDialogConfirmForm(array &$form, FormStateInterface $form
     return $form;
   }
 
+  /**
+   * Build webform dialog delete link.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param \Drupal\Core\Url $url
+   *   The delete URL.
+   */
+  protected function buildDialogDeleteAction(array &$form, FormStateInterface $form_state, Url $url) {
+    // WORKAROUND:
+    // Create a hidden link that is clicked using jQuery.
+    if ($this->isDialog()) {
+      $form['delete'] = [
+        '#type' => 'link',
+        '#title' => $this->t('Delete'),
+        '#url' => $url,
+        '#attributes' => ['style' => 'display:none'] + WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW, ['webform-dialog-delete-link']),
+      ];
+      $form['actions']['delete'] = [
+        '#type' => 'submit',
+        '#value' => $this->t('Delete'),
+        '#attributes' => [
+          'class' => ['button', 'button--danger'],
+          'onclick' => "jQuery('.webform-dialog-delete-link').click(); return false;",
+        ],
+      ];
+    }
+    else {
+      $form['actions']['delete'] = [
+        '#type' => 'link',
+        '#title' => $this->t('Delete'),
+        '#url' => $url,
+        '#attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW, ['button', 'button--danger']),
+      ];
+    }
+    WebformDialogHelper::attachLibraries($form);
+  }
+
   /****************************************************************************/
   // Ajax submit callbacks.
   /****************************************************************************/
@@ -83,11 +125,16 @@ public function cancelAjaxForm(array &$form, FormStateInterface $form_state) {
     return $response;
   }
 
+  /**
+   * Validate callback to clear validation errors.
+   */
+  public function noValidate(array &$form, FormStateInterface $form_state) {
+    // Clear all validation errors.
+    $form_state->clearErrors();
+  }
+
   /**
    * Empty submit callback used to only have the submit button to use an #ajax submit callback.
-   *
-   * This allows modal dialog to using ::submitCallback to validate and submit
-   * the form via one ajax request.
    */
   public function noSubmit(array &$form, FormStateInterface $form_state) {
     // Do nothing.
diff --git a/web/modules/webform/src/Form/WebformEntityFilterForm.php b/web/modules/webform/src/Form/WebformEntityFilterForm.php
index fc30e8ffd5776c2d2b481662f0b8fd461c45623a..3ff0291ec3e0730622b29b150df224cf3613ac12 100644
--- a/web/modules/webform/src/Form/WebformEntityFilterForm.php
+++ b/web/modules/webform/src/Form/WebformEntityFilterForm.php
@@ -5,10 +5,11 @@
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\WebformInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
- * Provides the webform filter webform.
+ * Provides the webform filter form.
  */
 class WebformEntityFilterForm extends FormBase {
 
@@ -58,11 +59,12 @@ public function buildForm(array $form, FormStateInterface $form_state, $search =
     ];
     $form['filter']['search'] = [
       '#type' => 'search',
-      '#title' => $this->t('Filter by title, description, elements, user name, or role'),
+      '#title' => $this->t('Keyword'),
       '#title_display' => 'invisible',
-      '#autocomplete_route_name' => 'entity.webform.autocomplete',
+      '#autocomplete_route_name' => 'entity.webform.autocomplete' . ($state === WebformInterface::STATUS_ARCHIVED ? '.archived' : ''),
       '#placeholder' => $this->t('Filter by title, description, elements, user name, or role'),
-      '#maxlength' => 128,
+      // Allow autocomplete to use long webform titles.
+      '#maxlength' => 500,
       '#size' => 45,
       '#default_value' => $search,
     ];
diff --git a/web/modules/webform/src/Form/WebformHandlerAddForm.php b/web/modules/webform/src/Form/WebformHandlerAddForm.php
index 031e9277c25c8d2cbf4fd2b7f14e27938b706215..a59c8093ff8b4af65c95651fa7be036c185eb120 100644
--- a/web/modules/webform/src/Form/WebformHandlerAddForm.php
+++ b/web/modules/webform/src/Form/WebformHandlerAddForm.php
@@ -2,9 +2,12 @@
 
 namespace Drupal\webform\Form;
 
+use Drupal\Component\Transliteration\TransliterationInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\webform\Plugin\WebformHandlerManagerInterface;
 use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformTokenManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 
@@ -23,10 +26,17 @@ class WebformHandlerAddForm extends WebformHandlerFormBase {
   /**
    * Constructs a WebformHandlerAddForm.
    *
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\Component\Transliteration\TransliterationInterface $transliteration
+   *   The transliteration helper.
+   * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
+   *   The webform token manager.
    * @param \Drupal\webform\Plugin\WebformHandlerManagerInterface $webform_handler
    *   The webform handler manager.
    */
-  public function __construct(WebformHandlerManagerInterface $webform_handler) {
+  public function __construct(LanguageManagerInterface $language_manager, TransliterationInterface $transliteration, WebformTokenManagerInterface $token_manager, WebformHandlerManagerInterface $webform_handler) {
+    parent::__construct($language_manager, $transliteration, $token_manager);
     $this->webformHandlerManager = $webform_handler;
   }
 
@@ -35,6 +45,9 @@ public function __construct(WebformHandlerManagerInterface $webform_handler) {
    */
   public static function create(ContainerInterface $container) {
     return new static(
+      $container->get('language_manager'),
+      $container->get('transliteration'),
+      $container->get('webform.token_manager'),
       $container->get('plugin.manager.webform.handler')
     );
   }
diff --git a/web/modules/webform/src/Form/WebformHandlerDeleteForm.php b/web/modules/webform/src/Form/WebformHandlerDeleteForm.php
index 6f841c95c6aeb00871c2c69b8a548eecfe99b639..9ea7c88c876695146210981d5ab094fbc5edce41 100644
--- a/web/modules/webform/src/Form/WebformHandlerDeleteForm.php
+++ b/web/modules/webform/src/Form/WebformHandlerDeleteForm.php
@@ -2,16 +2,13 @@
 
 namespace Drupal\webform\Form;
 
-use Drupal\Core\Form\ConfirmFormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\webform\WebformInterface;
 
 /**
  * Form for deleting a webform handler.
  */
-class WebformHandlerDeleteForm extends ConfirmFormBase {
-
-  use WebformDialogFormTrait;
+class WebformHandlerDeleteForm extends WebformDeleteFormBase {
 
   /**
    * The webform containing the webform handler to be deleted.
@@ -31,16 +28,56 @@ class WebformHandlerDeleteForm extends ConfirmFormBase {
    * {@inheritdoc}
    */
   public function getQuestion() {
-    return $this->t('Are you sure you want to delete the @handler handler from the %webform webform?', ['%webform' => $this->webform->label(), '@handler' => $this->webformHandler->label()]);
+    if ($this->isDialog()) {
+      $t_args = [
+        '@title' => $this->webformHandler->label(),
+      ];
+      return $this->t("Delete the '@title' handler?", $t_args);
+    }
+    else {
+      $t_args = [
+        '%webform' => $this->webform->label(),
+        '%title' => $this->webformHandler->label(),
+      ];
+      return $this->t('Delete the %title handler from the %webform webform?', $t_args);
+    }
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getConfirmText() {
-    return $this->t('Delete');
+  public function getWarning() {
+    $t_args = ['%title' => $this->webformHandler->label()];
+    return [
+      '#type' => 'webform_message',
+      '#message_type' => 'warning',
+      '#message_message' => $this->t('Are you sure you want to delete the %title handler?', $t_args) . '<br/>' .
+        '<strong>' . $this->t('This action cannot be undone.') . '</strong>',
+    ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return [
+      'title' => [
+        '#markup' => $this->t('This action will…'),
+      ],
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => [
+          $this->t('Remove this handler'),
+          $this->t('Cancel all pending actions'),
+        ],
+      ],
+    ];
+  }
+
+  /****************************************************************************/
+  // Form methods.
+  /****************************************************************************/
+
   /**
    * {@inheritdoc}
    */
@@ -62,9 +99,7 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
     $this->webform = $webform;
     $this->webformHandler = $this->webform->getHandler($webform_handler);
 
-    $form = parent::buildForm($form, $form_state);
-    $this->buildDialogConfirmForm($form, $form_state);
-    return $form;
+    return parent::buildForm($form, $form_state);
   }
 
   /**
@@ -72,7 +107,7 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
     $this->webform->deleteWebformHandler($this->webformHandler);
-    drupal_set_message($this->t('The webform handler %name has been deleted.', ['%name' => $this->webformHandler->label()]));
+    $this->messenger()->addStatus($this->t('The webform handler %name has been deleted.', ['%name' => $this->webformHandler->label()]));
     $form_state->setRedirectUrl($this->webform->toUrl('handlers'));
   }
 
diff --git a/web/modules/webform/src/Form/WebformHandlerEditForm.php b/web/modules/webform/src/Form/WebformHandlerEditForm.php
index c0aeee4a2aea6101ebf72d63feaead399eb2850f..89fd6a97c45bf1c62a219a890e1077cd224d2510 100644
--- a/web/modules/webform/src/Form/WebformHandlerEditForm.php
+++ b/web/modules/webform/src/Form/WebformHandlerEditForm.php
@@ -3,6 +3,7 @@
 namespace Drupal\webform\Form;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
 use Drupal\webform\WebformInterface;
 
 /**
@@ -16,6 +17,11 @@ class WebformHandlerEditForm extends WebformHandlerFormBase {
   public function buildForm(array $form, FormStateInterface $form_state, WebformInterface $webform = NULL, $webform_handler = NULL) {
     $form = parent::buildForm($form, $form_state, $webform, $webform_handler);
     $form['#title'] = $this->t('Edit @label handler', ['@label' => $this->webformHandler->label()]);
+
+    // Delete action.
+    $url = new Url('entity.webform.handler.delete_form', ['webform' => $webform->id(), 'webform_handler' => $this->webformHandler->getHandlerId()]);
+    $this->buildDialogDeleteAction($form, $form_state, $url);
+
     return $form;
   }
 
diff --git a/web/modules/webform/src/Form/WebformHandlerFormBase.php b/web/modules/webform/src/Form/WebformHandlerFormBase.php
index c215c2f45c14c7fe2eb3c132bef53deec459da0c..ed2376968ae72bd66bc2ec5894449fef402f9246 100644
--- a/web/modules/webform/src/Form/WebformHandlerFormBase.php
+++ b/web/modules/webform/src/Form/WebformHandlerFormBase.php
@@ -2,14 +2,18 @@
 
 namespace Drupal\webform\Form;
 
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\Component\Transliteration\TransliterationInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Form\SubformState;
-use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\webform\Plugin\WebformHandlerInterface;
 use Drupal\webform\Utility\WebformFormHelper;
 use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformTokenManagerInterface;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a base webform for webform handlers.
@@ -18,6 +22,32 @@ abstract class WebformHandlerFormBase extends FormBase {
 
   use WebformDialogFormTrait;
 
+  /**
+   * Machine name maxlenght.
+   */
+  const MACHINE_NAME_MAXLENGHTH = 64;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * The transliteration helper.
+   *
+   * @var \Drupal\Component\Transliteration\TransliterationInterface
+   */
+  protected $transliteration;
+
+  /**
+   * The token manager.
+   *
+   * @var \Drupal\webform\WebformTokenManagerInterface
+   */
+  protected $tokenManager;
+
   /**
    * The webform.
    *
@@ -39,6 +69,33 @@ public function getFormId() {
     return 'webform_handler_form';
   }
 
+  /**
+   * Constructs a WebformHandlerFormBase.
+   *
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\Component\Transliteration\TransliterationInterface $transliteration
+   *   The transliteration helper.
+   * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
+   *   The webform token manager.
+   */
+  public function __construct(LanguageManagerInterface $language_manager, TransliterationInterface $transliteration, WebformTokenManagerInterface $token_manager) {
+    $this->languageManager = $language_manager;
+    $this->transliteration = $transliteration;
+    $this->tokenManager = $token_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('language_manager'),
+      $container->get('transliteration'),
+      $container->get('webform.token_manager')
+    );
+  }
+
   /**
    * Form constructor.
    *
@@ -122,7 +179,7 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
     ];
     $form['general']['handler_id'] = [
       '#type' => 'machine_name',
-      '#maxlength' => 64,
+      '#maxlength' => static::MACHINE_NAME_MAXLENGHTH,
       '#description' => $this->t('A unique name for this handler instance. Must be alpha-numeric and underscore separated.'),
       '#default_value' => $this->webformHandler->getHandlerId() ?: $this->getUniqueMachineName($this->webformHandler),
       '#required' => TRUE,
@@ -189,13 +246,6 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
       '#value' => $request->query->has('weight') ? (int) $request->query->get('weight') : $this->webformHandler->getWeight(),
     ];
 
-    $form['actions'] = ['#type' => 'actions'];
-    $form['actions']['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Save'),
-      '#button_type' => 'primary',
-    ];
-
     // Build tabs.
     $tabs = [
       'conditions' => [
@@ -217,6 +267,20 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformIn
     ];
     $form = WebformFormHelper::buildTabs($form, $tabs);
 
+    $form['actions'] = ['#type' => 'actions'];
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Save'),
+      '#button_type' => 'primary',
+    ];
+
+    // Add token links below the form and on every tab.
+    $form['token_tree_link'] = $this->tokenManager->buildTreeElement();
+    if ($form['token_tree_link']) {
+      $form['token_tree_link'] += [
+        '#weight' => 101,
+      ];
+    }
     return $this->buildDialogForm($form, $form_state);
   }
 
@@ -264,35 +328,49 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
 
     if ($this instanceof WebformHandlerAddForm) {
       $this->webform->addWebformHandler($this->webformHandler);
-      drupal_set_message($this->t('The webform handler was successfully added.'));
+      $this->messenger()->addStatus($this->t('The webform handler was successfully added.'));
     }
     else {
       $this->webform->updateWebformHandler($this->webformHandler);
-      drupal_set_message($this->t('The webform handler was successfully updated.'));
+      $this->messenger()->addStatus($this->t('The webform handler was successfully updated.'));
     }
 
     $form_state->setRedirectUrl($this->webform->toUrl('handlers', ['query' => ['update' => $this->webformHandler->getHandlerId()]]));
   }
 
   /**
-   * Generates a unique machine name for a webform handler instance.
+   * Generates a unique translated machine name for a webform handler instance.
    *
    * @param \Drupal\webform\Plugin\WebformHandlerInterface $handler
    *   The webform handler.
    *
    * @return string
-   *   Returns the unique name.
+   *   Returns a unique machine based the handler's plugin label.
+   *
+   * @see \Drupal\Core\Render\Element\MachineName
+   * @see \Drupal\system\MachineNameController::transliterate
    */
   public function getUniqueMachineName(WebformHandlerInterface $handler) {
-    $suggestion = $handler->getPluginId();
+    // Get label which default to the plugin's label for new instances.
+    $label = (string) $this->webformHandler->label();
+
+    // Get current langcode.
+    $langcode = $this->languageManager->getCurrentLanguage()->getId();
+
+    // Get machine name.
+    $suggestion = $this->transliteration->transliterate($label, $langcode, '_', static::MACHINE_NAME_MAXLENGHTH);
+    $suggestion = mb_strtolower($suggestion);
+    $suggestion = preg_replace('@' . strtr('[^a-z0-9_]+', ['@' => '\@', chr(0) => '']) . '@', '_', $suggestion);
+
+    // Increment the machine name.
     $count = 1;
     $machine_default = $suggestion;
     $instance_ids = $this->webform->getHandlers()->getInstanceIds();
     while (isset($instance_ids[$machine_default])) {
       $machine_default = $suggestion . '_' . $count++;
     }
-    // Only return a suggestion if it is not the default plugin id.
-    return ($machine_default != $handler->getPluginId()) ? $machine_default : '';
+
+    return $machine_default;
   }
 
   /**
@@ -306,10 +384,29 @@ public function getUniqueMachineName(WebformHandlerInterface $handler) {
    */
   public function exists($handler_id) {
     $instance_ids = $this->webform->getHandlers()->getInstanceIds();
-
     return (isset($instance_ids[$handler_id])) ? TRUE : FALSE;
   }
 
+  /**
+   * Get the webform handler's webform.
+   *
+   * @return \Drupal\webform\WebformInterface
+   *   A webform.
+   */
+  public function getWebform() {
+    return $this->webform;
+  }
+
+  /**
+   * Get the webform handler.
+   *
+   * @return \Drupal\webform\Plugin\WebformHandlerInterface
+   *   A webform handler.
+   */
+  public function getWebformHandler() {
+    return $this->webformHandler;
+  }
+
   /**
    * Process handler webform errors in webform.
    *
diff --git a/web/modules/webform/src/Form/WebformHelpVideoForm.php b/web/modules/webform/src/Form/WebformHelpVideoForm.php
index a7240ed1475a22a3ccdb2b832ced1e4d22666f2a..4273391f4a9213e4e3157c880cfd1ce24849235e 100644
--- a/web/modules/webform/src/Form/WebformHelpVideoForm.php
+++ b/web/modules/webform/src/Form/WebformHelpVideoForm.php
@@ -86,14 +86,16 @@ public function buildForm(array $form, FormStateInterface $form_state, $id = NUL
     }
 
     // Related resources.
-    $form['resources'] = [
-      '#type' => 'details',
-      '#title' => $this->t('Additional resources'),
-      'links' => [
-        '#theme' => 'links',
-        '#links' => $this->helpManager->getVideoLinks($id),
-      ],
-    ];
+    if ($video_links = $this->helpManager->getVideoLinks($this->videoId)) {
+      $form['resources'] = [
+        '#type' => 'details',
+        '#title' => $this->t('Additional resources'),
+        'links' => [
+          '#theme' => 'links',
+          '#links' => $video_links,
+        ],
+      ];
+    }
 
     // Actions.
     if (isset($video['submit_label'])) {
diff --git a/web/modules/webform/src/Form/WebformOptionsFilterForm.php b/web/modules/webform/src/Form/WebformOptionsFilterForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..7c2bcc735fa4d75702c010604e52554e6da84ff5
--- /dev/null
+++ b/web/modules/webform/src/Form/WebformOptionsFilterForm.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Drupal\webform\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides the webform options filter form.
+ */
+class WebformOptionsFilterForm extends FormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'webform_options_filter_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $search = NULL, $category = NULL, array $categories = []) {
+    $form['#attributes'] = ['class' => ['webform-filter-form']];
+    $form['filter'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Filter options'),
+      '#open' => TRUE,
+      '#attributes' => ['class' => ['container-inline']],
+    ];
+    $form['filter']['search'] = [
+      '#type' => 'search',
+      '#title' => $this->t('Keyword'),
+      '#title_display' => 'invisible',
+      '#autocomplete_route_name' => 'entity.webform_options.autocomplete',
+      '#placeholder' => $this->t('Filter by title or options'),
+      '#size' => 45,
+      '#default_value' => $search,
+    ];
+    $form['filter']['category'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Category'),
+      '#title_display' => 'invisible',
+      '#options' => $categories,
+      '#empty_option' => ($category) ? $this->t('Show all options') : $this->t('Filter by category'),
+      '#default_value' => $category,
+    ];
+    $form['filter']['submit'] = [
+      '#type' => 'submit',
+      '#button_type' => 'primary',
+      '#value' => $this->t('Filter'),
+    ];
+    if (!empty($search) || !empty($category)) {
+      $form['filter']['reset'] = [
+        '#type' => 'submit',
+        '#submit' => ['::resetForm'],
+        '#value' => $this->t('Reset'),
+      ];
+    }
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $query = [
+      'search' => trim($form_state->getValue('search')),
+      'category' => trim($form_state->getValue('category')),
+    ];
+    $form_state->setRedirect($this->getRouteMatch()->getRouteName(), $this->getRouteMatch()->getRawParameters()->all(), [
+      'query' => $query ,
+    ]);
+  }
+
+  /**
+   * Resets the filter selection.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function resetForm(array &$form, FormStateInterface $form_state) {
+    $form_state->setRedirect($this->getRouteMatch()->getRouteName(), $this->getRouteMatch()->getRawParameters()->all());
+  }
+
+}
diff --git a/web/modules/webform/src/Form/WebformResultsClearForm.php b/web/modules/webform/src/Form/WebformResultsClearForm.php
index 8043fad37ab378969574e3b5e5d3c171fe65f39b..0f8509065b7ec791658f6eae911f9da00397a9ba 100644
--- a/web/modules/webform/src/Form/WebformResultsClearForm.php
+++ b/web/modules/webform/src/Form/WebformResultsClearForm.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\webform\Form;
 
+use Drupal\Core\Form\FormStateInterface;
+
 /**
  * Webform for webform results clear webform.
  */
@@ -18,13 +20,78 @@ public function getFormId() {
    * {@inheritdoc}
    */
   public function getQuestion() {
-    if ($this->sourceEntity) {
-      $t_args = ['%title' => $this->sourceEntity->label()];
+    $t_args = ['%label' => $this->getLabel()];
+    return $this->t('Clear all %label submissions?', $t_args);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $submission_total = $this->submissionStorage->getTotal($this->webform, $this->sourceEntity);
+    if ($submission_total) {
+      return parent::buildForm($form, $form_state);
     }
     else {
-      $t_args = ['%title' => $this->webform->label()];
+      $t_args = ['%label' => $this->getLabel()];
+      $form['message'] = [
+        '#type' => 'webform_message',
+        '#message_type' => 'error',
+        '#message_message' => $this->t('There are no %label submissions.', $t_args),
+      ];
+      return $form;
     }
-    return $this->t('Are you sure you want to delete all submissions to the %title webform?', $t_args);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWarning() {
+    $t_args = ['%label' => $this->getLabel()];
+    return [
+      '#type' => 'webform_message',
+      '#message_type' => 'warning',
+      '#message_message' => $this->t('Are you sure you want to clear all %label submissions?', $t_args) . '<br/>' .
+        '<strong>' . $this->t('This action cannot be undone.') . '</strong>',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    $submission_total = $this->submissionStorage->getTotal($this->webform, $this->sourceEntity);
+
+    $t_args = [
+      '%label' => $this->getLabel(),
+      '@total' => $submission_total,
+      '@submissions' => $this->formatPlural($submission_total, 'submission', 'submissions'),
+    ];
+
+    return [
+      'title' => [
+        '#markup' => $this->t('This action will…'),
+      ],
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => [
+          $this->t('Remove @total %label @submissions', $t_args),
+          ['#markup' => '<em>' . $this->t('Take a few minutes to complete') . '</em>'],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmInput() {
+    $t_args = ['%label' => $this->getLabel()];
+    return [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Yes, I want to clear all %label submissions', $t_args),
+      '#required' => TRUE,
+    ];
   }
 
   /**
@@ -38,13 +105,8 @@ public function getCancelUrl() {
    * {@inheritdoc}
    */
   public function getMessage() {
-    if ($this->sourceEntity) {
-      $t_args = ['%title' => $this->sourceEntity->label()];
-    }
-    else {
-      $t_args = ['%title' => $this->webform->label()];
-    }
-    $this->t('Webform %title submissions cleared.', $t_args);
+    $t_args = ['%label' => $this->getLabel()];
+    $this->t('Webform %label submissions cleared.', $t_args);
   }
 
 }
diff --git a/web/modules/webform/src/Form/WebformResultsCustomForm.php b/web/modules/webform/src/Form/WebformResultsCustomForm.php
index ddcc570f55f6a17b3e18c869e46bbc9334b2a980..c3cd752930348b04e825a1275d4d8deae5339703 100644
--- a/web/modules/webform/src/Form/WebformResultsCustomForm.php
+++ b/web/modules/webform/src/Form/WebformResultsCustomForm.php
@@ -123,12 +123,16 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $form['sort'] = [
       '#prefix' => '<div class="container-inline">',
       '#type' => 'select',
+      '#title' => $this->t('Sort by'),
+      '#title_display' => 'invisible',
       '#field_prefix' => $this->t('Sort by'),
       '#options' => $sort_options,
       '#default_value' => $sort,
     ];
     $form['direction'] = [
       '#type' => 'select',
+      '#title' => $this->t('Direction'),
+      '#title_display' => 'invisible',
       '#field_prefix' => ' ' . $this->t('in', [], ['context' => 'Sort by {sort} in {direction} order']) . ' ',
       '#field_suffix' => ' ' . $this->t('order', [], ['context' => 'Sort by {sort} in {direction} order']),
       '#options' => [
@@ -143,6 +147,8 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $limit = $this->webform->getState($this->getStateKey('limit'), NULL);
     $form['limit'] = [
       '#type' => 'select',
+      '#title' => $this->t('Results per page'),
+      '#title_display' => 'invisible',
       '#field_prefix' => $this->t('Show', [], ['context' => 'Show {limit} results per page']),
       '#field_suffix' => $this->t('results per page'),
       '#options' => [
@@ -151,10 +157,8 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         '100' => '100',
         '200' => '200',
         '500' => '500',
-        '1000' => '1000',
-        '0' => $this->t('All'),
       ],
-      '#default_value' => ($limit !== NULL) ? $limit : 50,
+      '#default_value' => ($limit !== NULL) ? $limit : 20,
     ];
 
     // Default configuration.
@@ -202,11 +206,31 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#default_value' => $format['element_format'],
     ];
 
+    // Submission settings.
+    $form['submission'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Submission settings'),
+    ];
+    // Get link types.
+    // @see entity.webform_submission.* route names.
+    $link_type_options = [
+      'canonical' => $this->t('View'),
+      'table' => $this->t('Table'),
+    ];
+    $form['submission']['link_type'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Link submissions to…'),
+      '#descriptions' => $this->t('Please note: Drafts will always be linked to submission form.'),
+      '#options' => $link_type_options,
+      '#default_value' => $this->webform->getState($this->getStateKey('link_type'), 'canonical'),
+    ];
+
     // Build actions.
     $form['actions']['#type'] = 'actions';
     $form['actions']['save'] = [
       '#type' => 'submit',
       '#value' => $this->t('Save'),
+      '#button_type' => 'primary',
     ];
     $form['actions']['delete'] = [
       '#type' => 'submit',
@@ -285,6 +309,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     $this->webform->setState($this->getStateKey('direction'), $form_state->getValue('direction'));
     $this->webform->setState($this->getStateKey('limit'), (int) $form_state->getValue('limit'));
     $this->webform->setState($this->getStateKey('format'), $form_state->getValue('format'));
+    $this->webform->setState($this->getStateKey('link_type'), $form_state->getValue('link_type'));
 
     // Set default.
     if (empty($this->sourceEntity)) {
@@ -292,7 +317,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     }
 
     // Display message.
-    drupal_set_message($this->t('The customized table has been saved.'));
+    $this->messenger()->addStatus($this->t('The customized table has been saved.'));
 
     // Set redirect.
     $redirect_url = $this->requestHandler->getUrl($this->webform, $this->sourceEntity, 'webform.results_submissions');
@@ -314,7 +339,7 @@ public function delete(array &$form, FormStateInterface $form_state) {
     $this->webform->deleteState($this->getStateKey('limit'));
     $this->webform->deleteState($this->getStateKey('default'));
     $this->webform->deleteState($this->getStateKey('format'));
-    drupal_set_message($this->t('The customized table has been reset.'));
+    $this->messenger()->addStatus($this->t('The customized table has been reset.'));
   }
 
   /**
diff --git a/web/modules/webform/src/Form/WebformResultsExportForm.php b/web/modules/webform/src/Form/WebformResultsExportForm.php
index 0d672768f3002e26d912ca9f958a84f9a813dd10..2964f1c5c9d5032793c95836b42e8dbf33627e1d 100644
--- a/web/modules/webform/src/Form/WebformResultsExportForm.php
+++ b/web/modules/webform/src/Form/WebformResultsExportForm.php
@@ -80,6 +80,11 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#access' => ($saved_options) ? TRUE : FALSE,
       '#submit' => ['::delete'],
     ];
+
+    // Disable single submit.
+    $form['#attributes']['class'][] = 'webform-remove-single-submit';
+    $form['#attached']['library'][] = 'webform/webform.form';
+
     return $form;
   }
 
@@ -121,7 +126,7 @@ public function save(array &$form, FormStateInterface $form_state) {
     // Save the export options to the webform's state.
     $export_options = $this->submissionExporter->getValuesFromInput($form_state->getValues());
     $this->submissionExporter->setWebformOptions($export_options);
-    drupal_set_message($this->t('The download settings have been saved.'));
+    $this->messenger()->addStatus($this->t('The download settings have been saved.'));
   }
 
   /**
@@ -134,7 +139,7 @@ public function save(array &$form, FormStateInterface $form_state) {
    */
   public function delete(array &$form, FormStateInterface $form_state) {
     $this->submissionExporter->deleteWebformOptions();
-    drupal_set_message($this->t('The download settings have been reset.'));
+    $this->messenger()->addStatus($this->t('The download settings have been reset.'));
   }
 
 }
diff --git a/web/modules/webform/src/Form/WebformSubmissionDeleteForm.php b/web/modules/webform/src/Form/WebformSubmissionDeleteForm.php
index d98f591fe6b38b4f4a0559a2eb01f787f8c8b812..0bc0170d7ceeb8b20796a77b161f14e9d00396b0 100644
--- a/web/modules/webform/src/Form/WebformSubmissionDeleteForm.php
+++ b/web/modules/webform/src/Form/WebformSubmissionDeleteForm.php
@@ -11,7 +11,9 @@
 /**
  * Provides a confirmation webform for deleting a webform submission.
  */
-class WebformSubmissionDeleteForm extends ContentEntityDeleteForm {
+class WebformSubmissionDeleteForm extends ContentEntityDeleteForm implements WebformDeleteFormInterface {
+
+  use WebformDialogFormTrait;
 
   /**
    * The webform entity.
@@ -20,7 +22,6 @@ class WebformSubmissionDeleteForm extends ContentEntityDeleteForm {
    */
   protected $webform;
 
-
   /**
    * The webform submission entity.
    *
@@ -71,7 +72,12 @@ public static function create(ContainerInterface $container) {
   public function buildForm(array $form, FormStateInterface $form_state) {
     list($this->webformSubmission, $this->sourceEntity) = $this->requestHandler->getWebformSubmissionEntities();
     $this->webform = $this->webformSubmission->getWebform();
-    return parent::buildForm($form, $form_state);
+
+    $form['warning'] = $this->getWarning();
+    $form = parent::buildForm($form, $form_state);
+    $form['description'] = $this->getDescription();
+
+    return $this->buildDialogConfirmForm($form, $form_state);
   }
 
   /**
@@ -80,7 +86,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
   protected function actions(array $form, FormStateInterface $form_state) {
     // Issue #2582295: Confirmation cancel links are incorrect if installed in
     // a subdirectory
-    // Work-around: Remove sudirectory from destination before generating
+    // Work-around: Remove subdirectory from destination before generating
     // actions.
     $request = $this->getRequest();
     $destination = $request->query->get('destination');
@@ -101,14 +107,67 @@ protected function actions(array $form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function getQuestion() {
-    return $this->t('Are you sure you want to delete @title?', ['@title' => $this->webformSubmission->label()]);
+    $t_args = [
+      '%label' => $this->getEntity()->label(),
+    ];
+    return $this->t('Delete %label?', $t_args);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWarning() {
+    $t_args = [
+      '@entity-type' => $this->getEntity()->getEntityType()->getLowercaseLabel(),
+      '%label' => $this->getEntity()->label(),
+    ];
+
+    return [
+      '#type' => 'webform_message',
+      '#message_type' => 'warning',
+      '#message_message' => $this->t('Are you sure you want to delete the %label @entity-type?', $t_args) . '<br/>' .
+        '<strong>' . $this->t('This action cannot be undone.') . '</strong>',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return [
+      'title' => [
+        '#markup' => $this->t('This action will…'),
+      ],
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => [
+          $this->t('Remove records from the database'),
+          $this->t('Delete any uploaded files'),
+          $this->t('Cancel all pending actions'),
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDetails() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmInput() {
+    return [];
   }
 
   /**
    * {@inheritdoc}
    */
   protected function getDeletionMessage() {
-    return $this->t('@title has been deleted.', ['@title' => $this->webformSubmission->label()]);
+    return $this->t('%label has been deleted.', ['%label' => $this->webformSubmission->label()]);
   }
 
   /**
diff --git a/web/modules/webform/src/Form/WebformSubmissionDeleteMultiple.php b/web/modules/webform/src/Form/WebformSubmissionDeleteMultiple.php
index a250e0ebf25eae486a03bc0ade3871dc58528385..ecb673f1924621ba46bee55d3a564059e95e433d 100644
--- a/web/modules/webform/src/Form/WebformSubmissionDeleteMultiple.php
+++ b/web/modules/webform/src/Form/WebformSubmissionDeleteMultiple.php
@@ -6,7 +6,7 @@
 use Drupal\Core\Form\ConfirmFormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
-use Drupal\user\PrivateTempStoreFactory;
+use Drupal\Core\TempStore\PrivateTempStoreFactory;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 
@@ -25,7 +25,7 @@ class WebformSubmissionDeleteMultiple extends ConfirmFormBase {
   /**
    * The tempstore factory.
    *
-   * @var \Drupal\user\PrivateTempStoreFactory
+   * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
    */
   protected $tempStoreFactory;
 
@@ -39,7 +39,7 @@ class WebformSubmissionDeleteMultiple extends ConfirmFormBase {
   /**
    * Constructs a WebformSubmissionDeleteMultiple object.
    *
-   * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory
+   * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
    *   The tempstore factory.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $manager
    *   The entity type manager.
@@ -54,7 +54,7 @@ public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityT
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('user.private_tempstore'),
+      $container->get('tempstore.private'),
       $container->get('entity_type.manager')
     );
   }
diff --git a/web/modules/webform/src/Form/WebformSubmissionFilterForm.php b/web/modules/webform/src/Form/WebformSubmissionFilterForm.php
index 03bd2353713256ccbe598e2260d8786d29f9f46f..0ddc78d42b2ac3440b82181edcf77d671c60cf2d 100644
--- a/web/modules/webform/src/Form/WebformSubmissionFilterForm.php
+++ b/web/modules/webform/src/Form/WebformSubmissionFilterForm.php
@@ -4,9 +4,10 @@
 
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\WebformInterface;
 
 /**
- * Provides the webform submission filter webform.
+ * Provides the webform submission filter form.
  */
 class WebformSubmissionFilterForm extends FormBase {
 
@@ -20,7 +21,7 @@ public function getFormId() {
   /**
    * {@inheritdoc}
    */
-  public function buildForm(array $form, FormStateInterface $form_state, $search = NULL, $state = NULL, array $state_options = []) {
+  public function buildForm(array $form, FormStateInterface $form_state, $search = NULL, $state = NULL, array $state_options = [], $source_entity = NULL, $source_entity_options = []) {
     $form['#attributes'] = ['class' => ['webform-filter-form']];
     $form['filter'] = [
       '#type' => 'details',
@@ -30,13 +31,36 @@ public function buildForm(array $form, FormStateInterface $form_state, $search =
     ];
     $form['filter']['search'] = [
       '#type' => 'search',
-      '#title' => $this->t('Filter by submitted data and/or notes'),
+      '#title' => $this->t('Keyword'),
       '#title_display' => 'invisible',
       '#placeholder' => $this->t('Filter by submitted data and/or notes'),
       '#maxlength' => 128,
       '#size' => 40,
       '#default_value' => $search,
     ];
+    if ($source_entity_options) {
+      if ($source_entity_options instanceof WebformInterface) {
+        $form['filter']['entity'] = [
+          '#type' => 'search',
+          '#title' => $this->t('Submitted to'),
+          '#title_display' => 'invisible',
+          '#autocomplete_route_name' => 'entity.webform.results.source_entity.autocomplete',
+          '#autocomplete_route_parameters' => ['webform' => $source_entity_options->id()],
+          '#placeholder' => $this->t('Enter submitted to…'),
+          '#size' => 20,
+          '#default_value' => $source_entity,
+        ];
+      }
+      else {
+        $form['filter']['entity'] = [
+          '#type' => 'select',
+          '#title' => $this->t('Submitted to'),
+          '#title_display' => 'invisible',
+          '#options' => ['' => $this->t('Filter by submitted to')] + $source_entity_options,
+          '#default_value' => $source_entity,
+        ];
+      }
+    }
     $form['filter']['state'] = [
       '#type' => 'select',
       '#title' => $this->t('State'),
@@ -66,7 +90,12 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     $query = [
       'search' => trim($form_state->getValue('search')),
       'state' => trim($form_state->getValue('state')),
+      'entity' => trim($form_state->getValue('entity')),
     ];
+    $query = array_filter($query);
+    if (!empty($query['entity']) && preg_match('#\(([^)]+)\)#', $query['entity'], $match)) {
+      $query['entity'] = $match[1];
+    }
     $form_state->setRedirect($this->getRouteMatch()->getRouteName(), $this->getRouteMatch()->getRawParameters()->all(), [
       'query' => $query ,
     ]);
diff --git a/web/modules/webform/src/Form/WebformSubmissionResendForm.php b/web/modules/webform/src/Form/WebformSubmissionResendForm.php
index a48336f783bd65c55272500d4d6c5e35eb990e1d..cd8de42f446a25836f83555fac5b59b7aea267d2 100644
--- a/web/modules/webform/src/Form/WebformSubmissionResendForm.php
+++ b/web/modules/webform/src/Form/WebformSubmissionResendForm.php
@@ -147,12 +147,12 @@ public function buildForm(array $form, FormStateInterface $form_state, WebformSu
     // Add submission navigation.
     $source_entity = $this->requestHandler->getCurrentSourceEntity('webform_submission');
     $form['navigation'] = [
-      '#theme' => 'webform_submission_navigation',
+      '#type' => 'webform_submission_navigation',
       '#webform_submission' => $webform_submission,
       '#weight' => -20,
     ];
     $form['information'] = [
-      '#theme' => 'webform_submission_information',
+      '#type' => 'webform_submission_information',
       '#webform_submission' => $webform_submission,
       '#source_entity' => $source_entity,
       '#weight' => -19,
@@ -179,7 +179,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     $t_args = [
       '%label' => $message_handler->label(),
     ];
-    drupal_set_message($this->t('Successfully re-sent %label.', $t_args));
+    $this->messenger()->addStatus($this->t('Successfully re-sent %label.', $t_args));
   }
 
   /**
diff --git a/web/modules/webform/src/Form/WebformSubmissionsDeleteFormBase.php b/web/modules/webform/src/Form/WebformSubmissionsDeleteFormBase.php
index 18b2aa175c7bdb13df213f69130425bb51b7cf06..d1e36e81e4b4d731f173cf3f39a1fbe611360443 100644
--- a/web/modules/webform/src/Form/WebformSubmissionsDeleteFormBase.php
+++ b/web/modules/webform/src/Form/WebformSubmissionsDeleteFormBase.php
@@ -4,7 +4,6 @@
 
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Form\ConfirmFormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformRequestInterface;
@@ -13,7 +12,7 @@
 /**
  * Base webform for deleting webform submission.
  */
-abstract class WebformSubmissionsDeleteFormBase extends ConfirmFormBase {
+abstract class WebformSubmissionsDeleteFormBase extends WebformDeleteFormBase {
 
   /**
    * Default number of submission to be deleted during batch processing.
@@ -36,6 +35,13 @@ abstract class WebformSubmissionsDeleteFormBase extends ConfirmFormBase {
    */
   protected $sourceEntity;
 
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
   /**
    * The webform submission storage.
    *
@@ -59,8 +65,11 @@ abstract class WebformSubmissionsDeleteFormBase extends ConfirmFormBase {
    *   The webform request handler.
    */
   public function __construct(EntityTypeManagerInterface $entity_type_manager, WebformRequestInterface $request_handler) {
+    $this->entityTypeManager = $entity_type_manager;
     $this->submissionStorage = $entity_type_manager->getStorage('webform_submission');
     $this->requestHandler = $request_handler;
+
+    list($this->webform, $this->sourceEntity) = $this->requestHandler->getWebformEntities();
   }
 
   /**
@@ -80,14 +89,6 @@ public function getConfirmText() {
     return $this->t('Clear');
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    list($this->webform, $this->sourceEntity) = $this->requestHandler->getWebformEntities();
-    return parent::buildForm($form, $form_state);
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -95,13 +96,31 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     $form_state->setRedirectUrl($this->getCancelUrl());
     if ($this->submissionStorage->getTotal($this->webform, $this->sourceEntity) < $this->getBatchLimit()) {
       $this->submissionStorage->deleteAll($this->webform, $this->sourceEntity);
-      drupal_set_message($this->getFinishedMessage());
+      $this->messenger()->addStatus($this->getFinishedMessage());
     }
     else {
       $this->batchSet($this->webform, $this->sourceEntity);
     }
   }
 
+  /**
+   * Get webform or source entity label.
+   *
+   * @return null|string
+   *   Webform or source entity label.
+   */
+  public function getLabel() {
+    if ($this->sourceEntity) {
+      return $this->sourceEntity->label();
+    }
+    elseif ($this->webform->label()) {
+      return $this->webform->label();
+    }
+    else {
+      return '';
+    }
+  }
+
   /**
    * Message to displayed after submissions are deleted.
    *
@@ -112,6 +131,10 @@ public function getFinishedMessage() {
     return $this->t('Webform submissions cleared.');
   }
 
+  /****************************************************************************/
+  // Batch API.
+  /****************************************************************************/
+
   /**
    * Batch API; Initialize batch operations.
    *
@@ -180,7 +203,7 @@ public function batchProcess(WebformInterface $webform = NULL, EntityInterface $
     // Track progress.
     $context['sandbox']['progress'] += $this->submissionStorage->deleteAll($webform, $entity, $this->getBatchLimit(), $max_sid);
 
-    $context['message'] = $this->t('Deleting @count of @total submissions...', ['@count' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']]);
+    $context['message'] = $this->t('Deleting @count of @total submissions…', ['@count' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']]);
 
     // Track finished.
     if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
@@ -200,10 +223,10 @@ public function batchProcess(WebformInterface $webform = NULL, EntityInterface $
    */
   public function batchFinish($success = FALSE, array $results, array $operations) {
     if (!$success) {
-      drupal_set_message($this->t('Finished with an error.'));
+      $this->messenger()->addStatus($this->t('Finished with an error.'));
     }
     else {
-      drupal_set_message($this->getFinishedMessage());
+      $this->messenger()->addStatus($this->getFinishedMessage());
     }
   }
 
diff --git a/web/modules/webform/src/Form/WebformSubmissionsPurgeForm.php b/web/modules/webform/src/Form/WebformSubmissionsPurgeForm.php
index 040c7650446e3cea124ab381e2badd51d28e9879..36e4088191edcbf0172cd5ed69b5a2b4516e9840 100644
--- a/web/modules/webform/src/Form/WebformSubmissionsPurgeForm.php
+++ b/web/modules/webform/src/Form/WebformSubmissionsPurgeForm.php
@@ -21,38 +21,57 @@ public function getFormId() {
    * {@inheritdoc}
    */
   public function getQuestion() {
-    return $this->t('Are you sure you want to delete all submissions?');
+    return $this->t('Purge all submissions?');
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getConfirmText() {
-    return $this->t('Purge');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCancelUrl() {
-    return new Url('entity.webform_submission.collection');
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $submission_total = $this->entityTypeManager
+      ->getStorage('webform_submission')
+      ->getQuery()
+      ->count()
+      ->execute();
+    if ($submission_total) {
+      return parent::buildForm($form, $form_state);
+    }
+    else {
+      $form['message'] = [
+        '#type' => 'webform_message',
+        '#message_type' => 'error',
+        '#message_message' => $this->t('There are no webform submissions.'),
+      ];
+      return $form;
+    }
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getFinishedMessage() {
-    return $this->t('Webform submissions purged.');
+  public function getWarning() {
+    return [
+      '#type' => 'webform_message',
+      '#message_type' => 'warning',
+      '#message_message' => $this->t('Are you sure you want to purge all submissions?') . '<br/>' .
+        '<strong>' . $this->t('This action cannot be undone.') . '</strong>',
+    ];
   }
 
   /**
    * {@inheritdoc}
    */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $form = parent::buildForm($form, $form_state);
-
-    $submission_total = \Drupal::entityQuery('webform_submission')->count()->execute();
-    $form_total = \Drupal::entityQuery('webform')->count()->execute();
+  public function getDescription() {
+    $submission_total = $this->entityTypeManager
+      ->getStorage('webform_submission')
+      ->getQuery()
+      ->count()
+      ->execute();
+    $form_total = $this->entityTypeManager
+      ->getStorage('webform')
+      ->getQuery()
+      ->count()
+      ->execute();
 
     $t_args = [
       '@submission_total' => $submission_total,
@@ -61,14 +80,50 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '@forms' => $this->formatPlural($form_total, 'webform', 'webforms'),
     ];
 
-    $form['confirm'] = [
+    return [
+      'title' => [
+        '#markup' => $this->t('This action will…'),
+      ],
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => [
+          $this->t('Remove @submission_total @submissions in @form_total @forms', $t_args),
+          ['#markup' => '<em>' . $this->t('Take a few minutes to complete') . '</em>'],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmInput() {
+    return [
       '#type' => 'checkbox',
-      '#title' => $this->t('Are you sure you want to delete @submission_total @submissions in @form_total @forms?', $t_args),
+      '#title' => $this->t('Yes, I want to purge all submissions'),
       '#required' => TRUE,
-      '#weight' => -10,
     ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Purge');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return new Url('entity.webform_submission.collection');
+  }
 
-    return $form;
+  /**
+   * {@inheritdoc}
+   */
+  public function getFinishedMessage() {
+    return $this->t('Webform submissions purged.');
   }
 
 }
diff --git a/web/modules/webform/src/Plugin/Action/DeleteWebformSubmission.php b/web/modules/webform/src/Plugin/Action/DeleteWebformSubmission.php
index d262a7233f06c9c07d40f56a523b429fe494817f..682839ad6b2047c62c623f67a82bf812280f55d6 100644
--- a/web/modules/webform/src/Plugin/Action/DeleteWebformSubmission.php
+++ b/web/modules/webform/src/Plugin/Action/DeleteWebformSubmission.php
@@ -5,7 +5,7 @@
 use Drupal\Core\Action\ActionBase;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Session\AccountInterface;
-use Drupal\user\PrivateTempStoreFactory;
+use Drupal\Core\TempStore\PrivateTempStoreFactory;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -23,7 +23,7 @@ class DeleteWebformSubmission extends ActionBase implements ContainerFactoryPlug
   /**
    * The tempstore factory.
    *
-   * @var \Drupal\user\PrivateTempStoreFactory
+   * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
    */
   protected $tempStoreFactory;
 
@@ -43,7 +43,7 @@ class DeleteWebformSubmission extends ActionBase implements ContainerFactoryPlug
    *   The plugin ID for the plugin instance.
    * @param mixed $plugin_definition
    *   The plugin implementation definition.
-   * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory
+   * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
    *   The tempstore factory.
    * @param \Drupal\Core\Session\AccountInterface $current_user
    *   The current user.
@@ -63,7 +63,7 @@ public static function create(ContainerInterface $container, array $configuratio
       $configuration,
       $plugin_id,
       $plugin_definition,
-      $container->get('user.private_tempstore'),
+      $container->get('tempstore.private'),
       $container->get('current_user')
     );
   }
diff --git a/web/modules/webform/src/Plugin/Block/WebformBlock.php b/web/modules/webform/src/Plugin/Block/WebformBlock.php
index 226b5aceeb8d9e194ac6fce1c3528d2e60baa75d..8a1282060fa6935d2a0c52458e14a33c161d9021 100644
--- a/web/modules/webform/src/Plugin/Block/WebformBlock.php
+++ b/web/modules/webform/src/Plugin/Block/WebformBlock.php
@@ -2,13 +2,16 @@
 
 namespace Drupal\webform\Plugin\Block;
 
+use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Block\BlockBase;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformTokenManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Provides a 'Webform' block.
@@ -21,6 +24,13 @@
  */
 class WebformBlock extends BlockBase implements ContainerFactoryPluginInterface {
 
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
   /**
    * Entity type manager.
    *
@@ -44,13 +54,16 @@ class WebformBlock extends BlockBase implements ContainerFactoryPluginInterface
    *   The plugin_id for the plugin instance.
    * @param mixed $plugin_definition
    *   The plugin implementation definition.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
-   *   The entity type manager service.
+   *   The entity type manager.
    * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
    *   The webform token manager.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, WebformTokenManagerInterface $token_manager) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, WebformTokenManagerInterface $token_manager) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->requestStack = $request_stack;
     $this->entityTypeManager = $entity_type_manager;
     $this->tokenManager = $token_manager;
   }
@@ -63,6 +76,7 @@ public static function create(ContainerInterface $container, array $configuratio
       $configuration,
       $plugin_id,
       $plugin_definition,
+      $container->get('request_stack'),
       $container->get('entity_type.manager'),
       $container->get('webform.token_manager')
     );
@@ -75,6 +89,7 @@ public function defaultConfiguration() {
     return [
       'webform_id' => '',
       'default_data' => '',
+      'redirect' => FALSE,
     ];
   }
 
@@ -91,13 +106,37 @@ public function blockForm($form, FormStateInterface $form_state) {
     ];
     $form['default_data'] = [
       '#title' => $this->t('Default webform submission data (YAML)'),
-      '#description' => $this->t('Enter webform submission data as name and value pairs which will be used to prepopulate the selected webform. You may use tokens.'),
       '#type' => 'webform_codemirror',
       '#mode' => 'yaml',
       '#default_value' => $this->configuration['default_data'],
+      '#webform_element' => TRUE,
+      '#description' => [
+        'content' => ['#markup' => $this->t('Enter submission data as name and value pairs as <a href=":href">YAML</a> which will be used to prepopulate the selected webform.', [':href' => 'https://en.wikipedia.org/wiki/YAML']), '#suffix' => ' '],
+        'token' => $this->tokenManager->buildTreeLink(),
+      ],
+      '#more_title' => $this->t('Example'),
+      '#more' => [
+        '#theme' => 'webform_codemirror',
+        '#type' => 'yaml',
+        '#code' => "# This is an example of a comment.
+element_key: 'some value'
+
+# The below example uses a token to get the current node's title.
+# Add ':clear' to the end token to return an empty value when the token is missing.
+title: '[webform_submission:node:title:clear]'
+# The below example uses a token to get a field value from the current node.
+full_name: '[webform_submission:node:field_full_name:clear]",
+      ],
+    ];
+    $form['redirect'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Redirect to the webform'),
+      '#default_value' => $this->configuration['redirect'],
+      '#return_value' => TRUE,
+      '#description' => $this->t('If your webform has multiple pages, this will change the behavior of the "Next" button. This will also affect where validation messages show up after an error.'),
     ];
 
-    $form['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['token_tree_link'] = $this->tokenManager->buildTreeElement();
 
     $this->tokenManager->elementValidate($form);
 
@@ -110,24 +149,46 @@ public function blockForm($form, FormStateInterface $form_state) {
   public function blockSubmit($form, FormStateInterface $form_state) {
     $this->configuration['webform_id'] = $form_state->getValue('webform_id');
     $this->configuration['default_data'] = $form_state->getValue('default_data');
+    $this->configuration['redirect'] = $form_state->getValue('redirect');
   }
 
   /**
    * {@inheritdoc}
    */
   public function build() {
-    return [
+    $build = [
       '#type' => 'webform',
       '#webform' => $this->getWebform(),
       '#default_data' => $this->configuration['default_data'],
     ];
+
+    // If redirect, set the #action property on the form.
+    if ($this->configuration['redirect']) {
+      $build['#action'] = $this->getWebform()->toUrl()
+        ->setOption('query', $this->requestStack->getCurrentRequest()->query->all())
+        ->toString();
+    }
+
+    return $build;
   }
 
   /**
    * {@inheritdoc}
    */
   protected function blockAccess(AccountInterface $account) {
-    return $this->getWebform()->access('submission_create', $account, TRUE);
+    $webform = $this->getWebform();
+    if (!$webform) {
+      return AccessResult::forbidden();
+    }
+
+    $access_result = $webform->access('submission_create', $account, TRUE);
+    if ($access_result->isAllowed()) {
+      return $access_result;
+    }
+
+    $has_access_denied_message = ($webform->getSetting('form_access_denied') !== WebformInterface::ACCESS_DENIED_DEFAULT);
+    return AccessResult::allowedIf($has_access_denied_message)
+      ->addCacheableDependency($access_result);
   }
 
   /**
@@ -142,14 +203,6 @@ public function calculateDependencies() {
     return $dependencies;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getCacheMaxAge() {
-    // Caching strategy is handled by the webform.
-    return 0;
-  }
-
   /**
    * Get this block instance webform.
    *
diff --git a/web/modules/webform/src/Plugin/Block/WebformSubmissionLimitBlock.php b/web/modules/webform/src/Plugin/Block/WebformSubmissionLimitBlock.php
index ca7153fc45ea320536faffa7f48266b06d4aa2a8..a1f3d15eefb9079acb68804ef7a48d8b1a8fb1d1 100644
--- a/web/modules/webform/src/Plugin/Block/WebformSubmissionLimitBlock.php
+++ b/web/modules/webform/src/Plugin/Block/WebformSubmissionLimitBlock.php
@@ -141,7 +141,7 @@ public function blockForm($form, FormStateInterface $form_state) {
       '#description' => self::buildTokens($this->configuration['type'], $this->configuration['source_entity']),
     ];
 
-    $form['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['token_tree_link'] = $this->tokenManager->buildTreeElement();
 
     $form['progress_bar'] = [
       '#title' => $this->t('Show progress bar'),
diff --git a/web/modules/webform/src/Plugin/Condition/Webform.php b/web/modules/webform/src/Plugin/Condition/Webform.php
index 20b06da85f98b2f8d59d70d6bf3b0a6e9f1630b6..62b1514b06e215863de5390bb5f5c4461a52cc4c 100644
--- a/web/modules/webform/src/Plugin/Condition/Webform.php
+++ b/web/modules/webform/src/Plugin/Condition/Webform.php
@@ -90,9 +90,10 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#type' => 'select',
       '#options' => $options,
       '#multiple' => $options,
+      '#select2' => TRUE,
       '#default_value' => $this->configuration['webforms'],
     ];
-    WebformElementHelper::enhanceSelect($form['webforms'], TRUE);
+    WebformElementHelper::process($form['webforms']);
 
     if (empty($this->configuration['context_mapping'])) {
       $form['message'] = [
@@ -130,7 +131,9 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
    * {@inheritdoc}
    */
   public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
-    $this->configuration['webforms'] = array_filter($form_state->getValue('webforms'));
+    $values = $form_state->getValue('webforms') ?: [];
+    $values = array_filter($values);
+    $this->configuration['webforms'] = array_combine($values, $values);
     parent::submitConfigurationForm($form, $form_state);
   }
 
diff --git a/web/modules/webform/src/Plugin/DevelGenerate/WebformSubmissionDevelGenerate.php b/web/modules/webform/src/Plugin/DevelGenerate/WebformSubmissionDevelGenerate.php
index 1ff1fb9f051aacbc608f399b685c6a4fb177de58..02015f0c228ae5faaf43a42d02f6ec23449073e6 100644
--- a/web/modules/webform/src/Plugin/DevelGenerate/WebformSubmissionDevelGenerate.php
+++ b/web/modules/webform/src/Plugin/DevelGenerate/WebformSubmissionDevelGenerate.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Serialization\Yaml;
 use Drupal\devel_generate\DevelGenerateBase;
@@ -89,6 +90,13 @@ class WebformSubmissionDevelGenerate extends DevelGenerateBase implements Contai
    */
   protected $webformEntityReferenceManager;
 
+  /**
+   * The messenger.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
   /**
    * Constructs a WebformSubmissionDevelGenerate object.
    *
@@ -99,25 +107,27 @@ class WebformSubmissionDevelGenerate extends DevelGenerateBase implements Contai
    * @param mixed $plugin_definition
    *   The plugin implementation definition.
    * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
-   *   The request stack service.
+   *   The request stack.
    * @param \Drupal\Core\Database\Connection $database
    *   The database.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    *   The entity type manager.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger.
    * @param \Drupal\webform\WebformSubmissionGenerateInterface $webform_submission_generate
    *   The webform submission generator.
    * @param \Drupal\webform\WebformEntityReferenceManagerInterface $webform_entity_reference_manager
    *   The webform entity reference manager.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, RequestStack $request_stack, Connection $database, EntityTypeManagerInterface $entity_type_manager, WebformSubmissionGenerateInterface $webform_submission_generate, WebformEntityReferenceManagerInterface $webform_entity_reference_manager) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, RequestStack $request_stack, Connection $database, EntityTypeManagerInterface $entity_type_manager, MessengerInterface $messenger, WebformSubmissionGenerateInterface $webform_submission_generate, WebformEntityReferenceManagerInterface $webform_entity_reference_manager) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
 
     $this->request = $request_stack->getCurrentRequest();
     $this->database = $database;
     $this->entityTypeManager = $entity_type_manager;
+    $this->messenger = $messenger;
     $this->webformSubmissionGenerate = $webform_submission_generate;
     $this->webformEntityReferenceManager = $webform_entity_reference_manager;
-
     $this->webformStorage = $entity_type_manager->getStorage('webform');
     $this->webformSubmissionStorage = $entity_type_manager->getStorage('webform_submission');
   }
@@ -127,10 +137,13 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
     return new static(
-      $configuration, $plugin_id, $plugin_definition,
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
       $container->get('request_stack'),
       $container->get('database'),
       $container->get('entity_type.manager'),
+      $container->get('messenger'),
       $container->get('webform_submission.generate'),
       $container->get('webform.entity_reference_manager')
     );
@@ -140,7 +153,11 @@ public static function create(ContainerInterface $container, array $configuratio
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state) {
-    drupal_set_message($this->t('Please note that no emails will be sent while generating webform submissions.'), 'warning');
+    $form['message'] = [
+      '#type' => 'webform_message',
+      '#message_message' => $this->t('Please note that no emails will be sent while generating webform submissions.'),
+      '#message_type' => 'warning',
+    ];
 
     $options = [];
     foreach ($this->webformStorage->loadMultiple() as $webform) {
@@ -197,7 +214,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) {
       $form['submitted']['entity-type'] = [
         '#type' => 'select',
         '#title' => $this->t('Entity type'),
-        '#title_display' => 'Invisible',
+        '#title_display' => 'invisible',
         '#empty_option' => $this->t('- None -'),
         '#options' => $entity_types,
         '#default_value' => $this->getSetting('entity-type'),
@@ -205,7 +222,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) {
       $form['submitted']['entity-id'] = [
         '#type' => 'number',
         '#title' => $this->t('Entity id'),
-        '#title_display' => 'Invisible',
+        '#title_display' => 'invisible',
         '#default_value' => $this->getSetting('entity-id'),
         '#min' => 1,
         '#size' => 10,
diff --git a/web/modules/webform/src/Plugin/EntityReferenceSelection/WebformSelection.php b/web/modules/webform/src/Plugin/EntityReferenceSelection/WebformSelection.php
index 8b08e1043af83a4827cc3fcadd7e2861680743ba..834af4e2eaea95184edfa6f4af9dd24ca4cba158 100644
--- a/web/modules/webform/src/Plugin/EntityReferenceSelection/WebformSelection.php
+++ b/web/modules/webform/src/Plugin/EntityReferenceSelection/WebformSelection.php
@@ -37,6 +37,8 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS')
     $query = parent::buildEntityQuery($match, $match_operator);
     // Exclude templates.
     $query->condition('template', FALSE);
+    // Exclude archived.
+    $query->condition('archive', FALSE);
     return $query;
   }
 
diff --git a/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceEntityFormatter.php b/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceEntityFormatter.php
index 862ac15f5c8a4889b74f9205c4564b7c8995ad1d..c13056eb3c0d3960d8c1a334b8a29db7687a9ed2 100644
--- a/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceEntityFormatter.php
+++ b/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceEntityFormatter.php
@@ -2,9 +2,18 @@
 
 namespace Drupal\webform\Plugin\Field\FieldFormatter;
 
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Serialization\Yaml;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\webform\WebformMessageManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Plugin implementation of the 'Webform rendered entity' formatter.
@@ -20,6 +29,83 @@
  */
 class WebformEntityReferenceEntityFormatter extends WebformEntityReferenceFormatterBase {
 
+  /**
+   * The route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The webform message manager.
+   *
+   * @var \Drupal\webform\WebformMessageManagerInterface
+   */
+  protected $messageManager;
+
+  /**
+   * WebformEntityReferenceEntityFormatter constructor.
+   *
+   * @param string $plugin_id
+   *   The plugin_id for the formatter.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the formatter is associated.
+   * @param array $settings
+   *   The formatter settings.
+   * @param string $label
+   *   The formatter label display setting.
+   * @param string $view_mode
+   *   The view mode.
+   * @param array $third_party_settings
+   *   Third party settings.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The factory for configuration objects.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The route match.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\webform\WebformMessageManagerInterface $message_manager
+   *   The webform message manager.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, RendererInterface $renderer, ConfigFactoryInterface $config_factory, RouteMatchInterface $route_match, EntityTypeManagerInterface $entity_type_manager, WebformMessageManagerInterface $message_manager) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $renderer, $config_factory);
+
+    $this->routeMatch = $route_match;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->messageManager = $message_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['label'],
+      $configuration['view_mode'],
+      $configuration['third_party_settings'],
+      $container->get('renderer'),
+      $container->get('config.factory'),
+      $container->get('current_route_match'),
+      $container->get('entity_type.manager'),
+      $container->get('webform.message_manager')
+    );
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -42,12 +128,20 @@ public function settingsSummary() {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state) {
-    $entity_type_definition = \Drupal::entityTypeManager()->getDefinition(
-      $this->fieldDefinition->getTargetEntityTypeId());
+    if ($this->fieldDefinition->getTargetEntityTypeId() === 'paragraph') {
+      $title = $this->t("Use this paragraph field's main entity as the webform submission's source entity.");
+      $description = $this->t("If unchecked, the current page's entity will be used as the webform submission's source entity.");
+    }
+    else {
+      $entity_type_definition = $this->entityTypeManager->getDefinition($this->fieldDefinition->getTargetEntityTypeId());
+      $title = $this->t("Use this field's %entity_type entity as the webform submission's source entity.", ['%entity_type' => $entity_type_definition->getLabel()]);
+      $description = $this->t("If unchecked, the current page's entity will be used as the webform submission's source entity. For example, if this webform was displayed on a node's page, the current node would be used as the webform submission's source entity.", ['%entity_type' => $entity_type_definition->getLabel()]);
+    }
+
     $form = parent::settingsForm($form, $form_state);
     $form['source_entity'] = [
-      '#title' => $this->t("Use this field's %entity_type entity as the webform submission's source entity.", ['%entity_type' => $entity_type_definition->getLabel()]),
-      '#description' => $this->t("If unchecked, the current page's entity will be used as the webform submission's source entity. For example, if this webform was displayed on a node's page, the current node would be used as the webform submission's source entity.", ['%entity_type' => $entity_type_definition->getLabel()]),
+      '#title' => $title,
+      '#description' => $description,
       '#type' => 'checkbox',
       '#return_type' => TRUE,
       '#default_value' => $this->getSetting('source_entity'),
@@ -59,20 +153,21 @@ public function settingsForm(array $form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function viewElements(FieldItemListInterface $items, $langcode) {
+    // Get source entity, which is the entity that the webform
+    // is directly attached to. For Paragraphs this would be the field's
+    // paragraph entity.
     $source_entity = $items->getEntity();
-    $this->messageManager->setSourceEntity($source_entity);
 
-    // Determine if webform is previewed within a Paragraph on .edit_form.
-    $is_paragraph_edit_preview = ($source_entity->getEntityTypeId() === 'paragraph' && preg_match('/\.edit_form$/', \Drupal::routeMatch()->getRouteName())) ? TRUE : FALSE;
+    // Determine if webform is previewed within a Paragraph on
+    // node edit forms (via *.edit_form or .content_translation_add routes).
+    $route = $this->routeMatch->getRouteName();
+    $is_node_edit = (preg_match('/\.edit_form$/', $route) || preg_match('/\.content_translation_add$/', $route));
+    $is_paragraph = ($source_entity && $source_entity->getEntityTypeId() === 'paragraph');
+    $is_paragraph_node_edit = ($is_paragraph && $is_node_edit);
 
     $elements = [];
     foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) {
-      // Do not display the webform if the current user can't
-      // create submissions.
-      if ($entity->id() && !$entity->access('submission_create')) {
-        $elements[$delta] = [];
-      }
-      elseif ($is_paragraph_edit_preview) {
+      if ($is_paragraph_node_edit) {
         // Webform can not be nested within node edit form because the nested
         // <form> tags will cause unexpected validation issues.
         $elements[$delta] = [
@@ -82,21 +177,26 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
         ];
       }
       else {
-        $values = [];
-        if ($this->getSetting('source_entity')) {
-          $values += [
-            'entity_type' => $source_entity->getEntityTypeId(),
-            'entity_id' => $source_entity->id(),
-          ];
-        }
-        if (!empty($items[$delta]->default_data)) {
-          $values['data'] = Yaml::decode($items[$delta]->default_data);
-        }
-        $elements[$delta] = $entity->getSubmissionForm($values);
+        $elements[$delta] = [
+          '#type' => 'webform',
+          '#webform' => $entity,
+          '#default_data' => (!empty($items[$delta]->default_data)) ? Yaml::decode($items[$delta]->default_data) : [],
+          '#entity' => ($this->getSetting('source_entity')) ? $source_entity : NULL,
+        ];
       }
       $this->setCacheContext($elements[$delta], $entity, $items[$delta]);
     }
     return $elements;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity) {
+    // Always allow access so that the Webform element can determine if the
+    // Webform is accessible or an access denied message should be displayed.
+    // @see \Drupal\webform\Element\Webform
+    return AccessResult::allowed();
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceFormatterBase.php b/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceFormatterBase.php
index 96fa8849c60812deb0628bbfdc33a849d8a5e2b1..fbf6bd86003a5a62163cda8fd72c21859ae44cb2 100644
--- a/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceFormatterBase.php
+++ b/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceFormatterBase.php
@@ -2,13 +2,14 @@
 
 namespace Drupal\webform\Plugin\Field\FieldFormatter;
 
+use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\webform\Plugin\Field\FieldType\WebformEntityReferenceItem;
 use Drupal\webform\WebformInterface;
-use Drupal\webform\WebformMessageManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -17,14 +18,21 @@
 abstract class WebformEntityReferenceFormatterBase extends EntityReferenceFormatterBase implements ContainerFactoryPluginInterface {
 
   /**
-   * The webform message manager.
+   * The renderer.
    *
-   * @var \Drupal\webform\WebformMessageManagerInterface
+   * @var \Drupal\Core\Render\RendererInterface
    */
-  protected $messageManager;
+  protected $renderer;
 
   /**
-   * WebformEntityReferenceEntityFormatter constructor.
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * WebformEntityReferenceLinkFormatter constructor.
    *
    * @param string $plugin_id
    *   The plugin_id for the formatter.
@@ -40,13 +48,16 @@ abstract class WebformEntityReferenceFormatterBase extends EntityReferenceFormat
    *   The view mode.
    * @param array $third_party_settings
    *   Third party settings.
-   * @param \Drupal\webform\WebformMessageManagerInterface $message_manager
-   *   The webform message manager.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The factory for configuration objects.
    */
-  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, WebformMessageManagerInterface $message_manager) {
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, RendererInterface $renderer, ConfigFactoryInterface $config_factory) {
     parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
 
-    $this->messageManager = $message_manager;
+    $this->configFactory = $config_factory;
+    $this->renderer = $renderer;
   }
 
   /**
@@ -61,7 +72,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $configuration['label'],
       $configuration['view_mode'],
       $configuration['third_party_settings'],
-      $container->get('webform.message_manager')
+      $container->get('renderer'),
+      $container->get('config.factory')
     );
   }
 
@@ -100,11 +112,11 @@ protected function getEntitiesToView(EntityReferenceFieldItemListInterface $item
    */
   protected function setCacheContext(array &$elements, WebformInterface $webform, WebformEntityReferenceItem $item) {
     // Track if webform.settings is updated.
-    $config = \Drupal::config('webform.settings');
-    \Drupal::service('renderer')->addCacheableDependency($elements, $config);
+    $config = $this->configFactory->get('webform.settings');
+    $this->renderer->addCacheableDependency($elements, $config);
 
-    // Track if the webfor is updated.
-    \Drupal::service('renderer')->addCacheableDependency($elements, $webform);
+    // Track if the webform is updated.
+    $this->renderer->addCacheableDependency($elements, $webform);
 
     // Calculate the max-age based on the open/close data/time for the item
     // and webform.
@@ -115,7 +127,7 @@ protected function setCacheContext(array &$elements, WebformInterface $webform,
         $item_state = $item->$state;
         if ($item_state && strtotime($item_state) > time()) {
           $item_seconds = strtotime($item_state) - time();
-          if (!$max_age || $item_seconds > $max_age) {
+          if (!$max_age && $item_seconds > $max_age) {
             $max_age = $item_seconds;
           }
         }
@@ -124,7 +136,7 @@ protected function setCacheContext(array &$elements, WebformInterface $webform,
         $webform_state = $webform->get($state);
         if ($webform_state && strtotime($webform_state) > time()) {
           $webform_seconds = strtotime($webform_state) - time();
-          if (!$max_age || $webform_seconds > $max_age) {
+          if (!$max_age && $webform_seconds > $max_age) {
             $max_age = $webform_seconds;
           }
         }
diff --git a/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceLinkFormatter.php b/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceLinkFormatter.php
index 8f6b2dcaa1e0ae5eeece3e942052d3596cc02332..7b4c316350ad37c2f3a7405e3941b4422b7a34c9 100644
--- a/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceLinkFormatter.php
+++ b/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceLinkFormatter.php
@@ -2,11 +2,14 @@
 
 namespace Drupal\webform\Plugin\Field\FieldFormatter;
 
+use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Utility\Token;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\webform\Element\WebformMessage;
 use Drupal\webform\WebformMessageManagerInterface;
+use Drupal\webform\WebformTokenManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -23,6 +26,13 @@
  */
 class WebformEntityReferenceLinkFormatter extends WebformEntityReferenceFormatterBase {
 
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
   /**
    * The webform message manager.
    *
@@ -31,11 +41,11 @@ class WebformEntityReferenceLinkFormatter extends WebformEntityReferenceFormatte
   protected $messageManager;
 
   /**
-   * The token service.
+   * The webform token manager.
    *
-   * @var \Drupal\Core\Utility\Token
+   * @var \Drupal\webform\WebformTokenManagerInterface
    */
-  protected $token;
+  protected $tokenManager;
 
   /**
    * WebformEntityReferenceLinkFormatter constructor.
@@ -54,15 +64,20 @@ class WebformEntityReferenceLinkFormatter extends WebformEntityReferenceFormatte
    *   The view mode.
    * @param array $third_party_settings
    *   Third party settings.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The factory for configuration objects.
    * @param \Drupal\webform\WebformMessageManagerInterface $message_manager
    *   The webform message manager.
-   * @param \Drupal\Core\Utility\Token $token
-   *   The token service.
+   * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
+   *   The webform token manager.
    */
-  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, WebformMessageManagerInterface $message_manager, Token $token) {
-    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $message_manager);
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, RendererInterface $renderer, ConfigFactoryInterface $config_factory, WebformMessageManagerInterface $message_manager, WebformTokenManagerInterface $token_manager) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $renderer, $config_factory);
 
-    $this->token = $token;
+    $this->messageManager = $message_manager;
+    $this->tokenManager = $token_manager;
   }
 
   /**
@@ -77,8 +92,10 @@ public static function create(ContainerInterface $container, array $configuratio
       $configuration['label'],
       $configuration['view_mode'],
       $configuration['third_party_settings'],
+      $container->get('renderer'),
+      $container->get('config.factory'),
       $container->get('webform.message_manager'),
-      $container->get('token')
+      $container->get('webform.token_manager')
     );
   }
 
@@ -88,6 +105,8 @@ public static function create(ContainerInterface $container, array $configuratio
   public static function defaultSettings() {
     return [
       'label' => 'Go to [webform:title] webform',
+      'dialog' => '',
+      'attributes' => [],
     ] + parent::defaultSettings();
   }
 
@@ -97,6 +116,10 @@ public static function defaultSettings() {
   public function settingsSummary() {
     $summary = parent::settingsSummary();
     $summary[] = $this->t('Label: @label', ['@label' => $this->getSetting('label')]);
+    $dialog_option_name = $this->getSetting('dialog');
+    if ($dialog_option = $this->configFactory->get('webform.settings')->get('settings.dialog_options.' . $dialog_option_name)) {
+      $summary[] = $this->t('Dialog: @dialog', ['@dialog' => (isset($dialog_option['title']) ? $dialog_option['title'] : $dialog_option_name)]);
+    }
     return $summary;
   }
 
@@ -105,12 +128,43 @@ public function settingsSummary() {
    */
   public function settingsForm(array $form, FormStateInterface $form_state) {
     $form = parent::settingsForm($form, $form_state);
+
+    if ($this->fieldDefinition->getTargetEntityTypeId() === 'paragraph') {
+      $form['message'] = [
+        '#type' => 'webform_message',
+        '#message_type' => 'warning',
+        '#message_message' => $this->t("This paragraph field's main entity will be used as the webform submission's source entity."),
+        '#message_close' => TRUE,
+        '#message_storage' => WebformMessage::STORAGE_SESSION,
+      ];
+    }
+
     $form['label'] = [
       '#title' => $this->t('Label'),
       '#type' => 'textfield',
       '#default_value' => $this->getSetting('label'),
       '#required' => TRUE,
     ];
+
+    $dialog_options = $this->configFactory->get('webform.settings')->get('settings.dialog_options');
+    if ($dialog_options) {
+      $options = [];
+      foreach ($dialog_options as $dialog_option_name => $dialog_option) {
+        $options[$dialog_option_name] = (isset($dialog_option['title'])) ? $dialog_option['title'] : $dialog_option_name;
+      }
+      $form['dialog'] = [
+        '#title' => $this->t('Dialog'),
+        '#type' => 'select',
+        '#empty_option' => t('- Select dialog -'),
+        '#default_value' => $this->getSetting('dialog'),
+        '#options' => $options,
+      ];
+      $form['attributes'] = [
+        '#type' => 'webform_element_attributes',
+        '#title' => $this->t('Link'),
+        '#default_value' => $this->getSetting('attributes'),
+      ];
+    }
     return $form;
   }
 
@@ -138,13 +192,23 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
             'source_entity_id' => $source_entity->id(),
           ],
         ];
-        $elements[$delta] = [
+        $link = [
           '#type' => 'link',
-          '#title' => $this->t($this->token->replace($this->getSetting('label'), [
-            'webform' => $entity,
-          ])),
+          '#title' => ['#markup' => $this->tokenManager->replace($this->getSetting('label'), $entity)],
           '#url' => $entity->toUrl('canonical', $link_options),
+          '#attributes' => $this->getSetting('attributes') ?: [],
         ];
+        if ($dialog = $this->getSetting('dialog')) {
+          $link['#attributes']['class'][] = 'webform-dialog';
+          $link['#attributes']['class'][] = 'webform-dialog-' . $dialog;
+          // Attach webform dialog library and options if they are not
+          // on every page.
+          if (!\Drupal::config('webform.settings')->get('settings.dialog')) {
+            $link['#attached']['library'][] = 'webform/webform.dialog';
+            $link['#attached']['drupalSettings']['webform']['dialog']['options'] = \Drupal::config('webform.settings')->get('settings.dialog_options');
+          }
+        }
+        $elements[$delta] = $link;
       }
       else {
         $this->messageManager->setWebform($entity);
diff --git a/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceUrlFormatter.php b/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceUrlFormatter.php
new file mode 100644
index 0000000000000000000000000000000000000000..cfaf261321fa12a972e6ec2a42b97e8e57f6de07
--- /dev/null
+++ b/web/modules/webform/src/Plugin/Field/FieldFormatter/WebformEntityReferenceUrlFormatter.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\webform\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FieldItemListInterface;
+
+/**
+ * Plugin implementation of the 'Webform url' formatter.
+ *
+ * @FieldFormatter(
+ *   id = "webform_entity_reference_url",
+ *   label = @Translation("URL"),
+ *   description = @Translation("Display URL to the referenced webform."),
+ *   field_types = {
+ *     "webform"
+ *   }
+ * )
+ */
+class WebformEntityReferenceUrlFormatter extends WebformEntityReferenceFormatterBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode) {
+    $source_entity = $items->getEntity();
+
+    $elements = [];
+
+    /** @var \Drupal\webform\WebformInterface[] $entities */
+    $entities = $this->getEntitiesToView($items, $langcode);
+
+    foreach ($entities as $delta => $entity) {
+      $link_options = [
+        'query' => [
+          'source_entity_type' => $source_entity->getEntityTypeId(),
+          'source_entity_id' => $source_entity->id(),
+        ],
+      ];
+
+      $link = [
+        '#plain_text' => $entity->toUrl('canonical', $link_options)->toString(),
+      ];
+
+      $elements[$delta] = $link;
+
+      $this->setCacheContext($elements[$delta], $entity, $items[$delta]);
+    }
+
+    return $elements;
+  }
+
+}
diff --git a/web/modules/webform/src/Plugin/Field/FieldType/WebformEntityReferenceItem.php b/web/modules/webform/src/Plugin/Field/FieldType/WebformEntityReferenceItem.php
index 13125bdb36a081955983f280514b9f54c9c138a7..cafeb981f5cfd79d70b25d11cc496691180b0eba 100644
--- a/web/modules/webform/src/Plugin/Field/FieldType/WebformEntityReferenceItem.php
+++ b/web/modules/webform/src/Plugin/Field/FieldType/WebformEntityReferenceItem.php
@@ -6,8 +6,8 @@
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\TypedData\DataDefinition;
-use Drupal\webform\WebformInterface;
 
 /**
  * Defines the 'webform_entity_reference' entity field type.
@@ -35,18 +35,6 @@ public static function defaultStorageSettings() {
     ] + parent::defaultStorageSettings();
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function defaultFieldSettings() {
-    return [
-      'default_data' => '',
-      'status' => WebformInterface::STATUS_OPEN,
-      'open' => '',
-      'close' => '',
-    ] + parent::defaultFieldSettings();
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -120,4 +108,13 @@ public static function getPreconfiguredOptions() {
     return [];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getSettableOptions(AccountInterface $account = NULL) {
+    /** @var \Drupal\webform\WebformEntityStorageInterface $webform_storage */
+    $webform_storage = \Drupal::service('entity_type.manager')->getStorage('webform');
+    return $webform_storage->getOptions(FALSE);
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/Field/FieldWidget/WebformEntityReferenceAutocompleteWidget.php b/web/modules/webform/src/Plugin/Field/FieldWidget/WebformEntityReferenceAutocompleteWidget.php
index 3662fd16742feaa2c7e156249b2274e4e868857e..a13cff27e8b4752130c63ea3cfcbe83caa08a2d0 100644
--- a/web/modules/webform/src/Plugin/Field/FieldWidget/WebformEntityReferenceAutocompleteWidget.php
+++ b/web/modules/webform/src/Plugin/Field/FieldWidget/WebformEntityReferenceAutocompleteWidget.php
@@ -2,12 +2,9 @@
 
 namespace Drupal\webform\Plugin\Field\FieldWidget;
 
-use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Field\Plugin\Field\FieldWidget\EntityReferenceAutocompleteWidget;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\webform\Utility\WebformDateHelper;
-use Drupal\webform\WebformInterface;
 
 /**
  * Plugin implementation of the 'webform_entity_reference_autocomplete' widget.
@@ -23,204 +20,32 @@
  */
 class WebformEntityReferenceAutocompleteWidget extends EntityReferenceAutocompleteWidget {
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function defaultSettings() {
-    return [
-      'default_data' => TRUE,
-    ] + parent::defaultSettings();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function settingsForm(array $form, FormStateInterface $form_state) {
-    $element = parent::settingsForm($form, $form_state);
-    $element['default_data'] = [
-      '#type' => 'checkbox',
-      '#title' => t('Enable default submission data (YAML)'),
-      '#description' => t('If checked, site builders will be able to define default submission data (YAML)'),
-      '#default_value' => $this->getSetting('default_data'),
-    ];
-    return $element;
-  }
+  use WebformEntityReferenceWidgetTrait;
 
   /**
    * {@inheritdoc}
    */
-  public function settingsSummary() {
-    $summary = parent::settingsSummary();
-    $summary[] = t('Default submission data: @default_data', ['@default_data' => $this->getSetting('default_data') ? $this->t('Yes') : $this->t('No')]);
-    return $summary;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
-    if (!isset($items[$delta]->status)) {
-      $items[$delta]->status = WebformInterface::STATUS_OPEN;
-    }
-
-    $element = parent::formElement($items, $delta, $element, $form, $form_state);
+  public function getTargetIdElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    // Get default value.
+    $referenced_entities = $items->referencedEntities();
+    $default_value = isset($referenced_entities[$delta]) ? $referenced_entities[$delta] : NULL;
 
-    // Get field name.
-    $field_name = $items->getName();
+    // Append the match operation to the selection settings.
+    $selection_settings = $this->getFieldSetting('handler_settings') + ['match_operator' => $this->getSetting('match_operator')];
 
-    // Get field input name from field parents, field name, and the delta.
-    $field_parents = array_merge($element['target_id']['#field_parents'], [$field_name, $delta]);
-    $field_input_name = (array_shift($field_parents)) . ('[' . implode('][', $field_parents) . ']');
-
-    // Set element 'target_id' default properties.
-    $element['target_id'] += [
-      '#weight' => 0,
-    ];
-
-    // Get weight.
-    $weight = $element['target_id']['#weight'];
-
-    $element['settings'] = [
-      '#type' => 'details',
-      '#title' => $this->t('@title settings', ['@title' => $element['target_id']['#title']]),
-      '#element_validate' => [[$this, 'validateOpenClose']],
-      '#open' => ($items[$delta]->target_id) ? TRUE : FALSE,
-      '#weight' => $weight++,
-    ];
-
-    $element['settings']['status'] = [
-      '#type' => 'radios',
-      '#title' => $this->t('Status'),
-      '#options' => [
-        WebformInterface::STATUS_OPEN => $this->t('Open'),
-        WebformInterface::STATUS_CLOSED => $this->t('Closed'),
-        WebformInterface::STATUS_SCHEDULED => $this->t('Scheduled'),
-      ],
-      '#options_display' => 'side_by_side',
-      '#default_value' => $items[$delta]->status,
-    ];
-
-    $element['settings']['scheduled'] = [
-      '#type' => 'item',
-      '#title' => $element['target_id']['#title'],
-      '#title_display' => 'invisible',
-      '#input' => FALSE,
-      '#states' => [
-        'visible' => [
-          'input[name="' . $field_input_name . '[settings][status]"]' => ['value' => WebformInterface::STATUS_SCHEDULED],
-        ],
-      ],
-    ];
-    $element['settings']['scheduled']['open'] = [
-      '#type' => 'datetime',
-      '#title' => $this->t('Open'),
-      '#default_value' => $items[$delta]->open ? DrupalDateTime::createFromTimestamp(strtotime($items[$delta]->open)) : NULL,
-      '#prefix' => '<div class="container-inline form-item">',
-      '#suffix' => '</div>',
-      '#help' => FALSE,
-      '#description' => [
-        '#type' => 'webform_help',
-        '#help' => $this->t('If the open date/time is left blank, this form will immediately be opened.'),
-      ],
-    ];
-    $element['settings']['scheduled']['close'] = [
-      '#type' => 'datetime',
-      '#title' => $this->t('Close'),
-      '#default_value' => $items[$delta]->close ? DrupalDateTime::createFromTimestamp(strtotime($items[$delta]->close)) : NULL,
-      '#prefix' => '<div class="container-inline form-item">',
-      '#suffix' => '</div>',
-      '#help' => FALSE,
-      '#description' => [
-        '#type' => 'webform_help',
-        '#help' => $this->t('If the close date/time is left blank, this webform will never be closed.'),
-      ],
+    return [
+      '#type' => 'entity_autocomplete',
+      '#target_type' => $this->getFieldSetting('target_type'),
+      '#selection_handler' => $this->getFieldSetting('handler'),
+      '#selection_settings' => $selection_settings,
+      // Entity reference field items are handling validation themselves via
+      // the 'ValidReference' constraint.
+      '#validate_reference' => FALSE,
+      '#maxlength' => 1024,
+      '#default_value' => $default_value,
+      '#size' => $this->getSetting('size'),
+      '#placeholder' => $this->getSetting('placeholder'),
     ];
-
-    if ($this->getSetting('default_data')) {
-      $element['settings']['default_data'] = [
-        '#type' => 'webform_codemirror',
-        '#mode' => 'yaml',
-        '#title' => $this->t('Default submission data (YAML)'),
-        '#description' => $this->t('Enter submission data as name and value pairs which will be used to prepopulate the selected webform. You may use tokens.'),
-        '#default_value' => $items[$delta]->default_data,
-      ];
-      /** @var \Drupal\webform\WebformTokenManagerInterface $token_manager */
-      $token_manager = \Drupal::service('webform.token_manager');
-      $element['settings']['token_tree_link'] = $token_manager->buildTreeLink();
-    }
-
-    return $element;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
-    parent::massageFormValues($values, $form, $form_state);
-
-    // Massage open/close dates.
-    // @see \Drupal\webform\WebformEntitySettingsForm::save
-    // @see \Drupal\datetime\Plugin\Field\FieldWidget\DateTimeWidgetBase::massageFormValues
-    foreach ($values as &$item) {
-      $item += $item['settings'];
-      unset($item['settings']);
-
-      if ($item['status'] === WebformInterface::STATUS_SCHEDULED) {
-        $states = ['open', 'close'];
-        foreach ($states as $state) {
-          if (!empty($item['scheduled'][$state]) && $item['scheduled'][$state] instanceof DrupalDateTime) {
-            $item[$state] = WebformDateHelper::formatStorage($item['scheduled'][$state]);
-          }
-          else {
-            $item[$state] = '';
-          }
-        }
-      }
-      else {
-        $item['open'] = '';
-        $item['close'] = '';
-      }
-      unset($item['scheduled']);
-    }
-    return $values;
-  }
-
-  /**
-   * Validate callback to ensure that the open date <= the close date.
-   *
-   * @param array $element
-   *   An associative array containing the properties and children of the
-   *   generic form element.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   * @param array $complete_form
-   *   The complete form structure.
-   *
-   * @see \Drupal\webform\WebformEntitySettingsForm::validateForm
-   * @see \Drupal\datetime_range\Plugin\Field\FieldWidget\DateRangeWidgetBase::validateOpenClose
-   */
-  public function validateOpenClose(array &$element, FormStateInterface $form_state, array &$complete_form) {
-    $status = $element['status']['#value'];
-    if ($status === WebformInterface::STATUS_SCHEDULED) {
-      $open_date = $element['scheduled']['open']['#value']['object'];
-      $close_date = $element['scheduled']['close']['#value']['object'];
-
-      // Require open or close dates.
-      if (empty($open_date) && empty($close_date)) {
-        $form_state->setError($element['scheduled']['open'], $this->t('Please enter an open or close date'));
-        $form_state->setError($element['scheduled']['close'], $this->t('Please enter an open or close date'));
-      }
-
-      // Make sure open date is not after close date.
-      if ($open_date instanceof DrupalDateTime && $close_date instanceof DrupalDateTime) {
-        if ($open_date->getTimestamp() !== $close_date->getTimestamp()) {
-          $interval = $open_date->diff($close_date);
-          if ($interval->invert === 1) {
-            $form_state->setError($element['scheduled']['open'], $this->t('The @title close date cannot be before the open date', ['@title' => $element['#title']]));
-          }
-        }
-      }
-    }
   }
 
 }
diff --git a/web/modules/webform/src/Plugin/Field/FieldWidget/WebformEntityReferenceSelectWidget.php b/web/modules/webform/src/Plugin/Field/FieldWidget/WebformEntityReferenceSelectWidget.php
index 9741db399fdf14e20be6e3c6ddc049f69448e7f6..83fd6a4068d2b1b26566360eb0d0df934c8f7e23 100644
--- a/web/modules/webform/src/Plugin/Field/FieldWidget/WebformEntityReferenceSelectWidget.php
+++ b/web/modules/webform/src/Plugin/Field/FieldWidget/WebformEntityReferenceSelectWidget.php
@@ -2,9 +2,12 @@
 
 namespace Drupal\webform\Plugin\Field\FieldWidget;
 
+use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Entity\Webform;
 use Drupal\webform\WebformInterface;
+use Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsWidgetBase;
 
 /**
  * Plugin implementation of the 'webform_entity_reference_select' widget.
@@ -18,76 +21,50 @@
  *   }
  * )
  *
- * @see \Drupal\webform\Plugin\Field\FieldWidget\WebformEntityReferenceAutocompleteWidget
- * @see \Drupal\Core\Field\Plugin\Field\FieldWidget\EntityReferenceAutocompleteWidget
- * @see \Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsSelectWidget
+ * @see \Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsWidgetBase
  */
-class WebformEntityReferenceSelectWidget extends WebformEntityReferenceAutocompleteWidget {
+class WebformEntityReferenceSelectWidget extends OptionsWidgetBase {
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function defaultSettings() {
-    return [
-      'default_data' => TRUE,
-    ];
-  }
+  use WebformEntityReferenceWidgetTrait;
 
   /**
    * {@inheritdoc}
    */
-  public function settingsForm(array $form, FormStateInterface $form_state) {
-    $element = [];
-    $element['default_data'] = [
-      '#type' => 'checkbox',
-      '#title' => t('Enable default submission data (YAML)'),
-      '#description' => t('If checked, site builders will be able to define default submission data (YAML)'),
-      '#default_value' => $this->getSetting('default_data'),
-    ];
-    return $element;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function settingsSummary() {
-    $summary = [];
-    $summary[] = t('Default submission data: @default_data', ['@default_data' => $this->getSetting('default_data') ? $this->t('Yes') : $this->t('No')]);
-    return $summary;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
-    $element = parent::formElement($items, $delta, $element, $form, $form_state);
+  public function getTargetIdElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    // Get default value (webform ID).
+    $referenced_entities = $items->referencedEntities();
+    $default_value = isset($referenced_entities[$delta]) ? $referenced_entities[$delta] : NULL;
+    // Convert default_value's Webform to a simple entity_id.
+    if ($default_value instanceof WebformInterface) {
+      $default_value = $default_value->id();
+    }
 
-    // Convert 'entity_autocomplete' to 'webform_entity_select' element.
-    $element['target_id']['#type'] = 'webform_entity_select';
+    // Get options grouped by category.
+    $options = $this->getOptions($items->getEntity());
+    // Make sure if an archived webform is the #default_value always include
+    // it as an option.
+    if ($default_value && $webform = Webform::load($default_value)) {
+      if ($webform->isArchived()) {
+        $options[(string) t('Archived')][$webform->id()] = $webform->label();
+      }
+    }
 
-    /** @var \Drupal\webform\WebformEntityStorageInterface $webform_storage */
-    $webform_storage = \Drupal::service('entity_type.manager')->getStorage('webform');
-    $element['target_id']['#options'] = $webform_storage->getOptions(FALSE);
+    $target_element = [
+      '#type' => 'webform_entity_select',
+      '#options' => $options,
+      '#default_value' => $default_value,
+    ];
 
     // Set empty option.
     if (empty($element['#required'])) {
-      $element['target_id']['#empty_option'] = $this->t('- Select -');
-      $element['target_id']['#empty_value'] = '';
-    }
-
-    // Convert default_value's Webform to a simple entity_id.
-    if (!empty($element['target_id']['#default_value']) && $element['target_id']['#default_value'] instanceof WebformInterface) {
-      $element['target_id']['#default_value'] = $element['target_id']['#default_value']->id();
+      $target_element['#empty_option'] = $this->t('- Select -');
+      $target_element['#empty_value'] = '';
     }
 
-    // Remove properties that are not applicable.
-    unset($element['target_id']['#size']);
-    unset($element['target_id']['#maxlength']);
-    unset($element['target_id']['#placeholder']);
-
-    $element['#element_validate'] = [[get_class($this), 'validateWebformEntityReferenceSelectWidget']];
+    // Set validation callback.
+    $target_element['#element_validate'] = [[get_class($this), 'validateWebformEntityReferenceSelectWidget']];
 
-    return $element;
+    return $target_element;
   }
 
   /**
@@ -97,8 +74,41 @@ public static function validateWebformEntityReferenceSelectWidget(&$element, For
     // Below prevents the below error.
     // Fatal error: Call to a member function uuid() on a non-object in
     // core/lib/Drupal/Core/Field/EntityReferenceFieldItemList.php.
-    $value = (!empty($element['target_id']['#value'])) ? $element['target_id']['#value'] : NULL;
-    $form_state->setValueForElement($element['target_id'], $value);
+    $value = (!empty($element['#value'])) ? $element['#value'] : NULL;
+    $form_state->setValueForElement($element, $value);
+  }
+
+  /**
+   * Returns the array of options for the widget.
+   *
+   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
+   *   The entity for which to return options.
+   *
+   * @return array
+   *   The array of options for the widget.
+   */
+  protected function getOptions(FieldableEntityInterface $entity) {
+    if (!isset($this->options)) {
+      // Limit the settable options for the current user account.
+      // Note: All active webforms are returned and grouped by category.
+      // @see \Drupal\webform\Plugin\Field\FieldType\WebformEntityReferenceItem::getSettableOptions
+      // @see \Drupal\webform\WebformEntityStorageInterface::getOptions
+      $options = $this->fieldDefinition
+        ->getFieldStorageDefinition()
+        ->getOptionsProvider($this->column, $entity)
+        ->getSettableOptions(\Drupal::currentUser());
+
+      $module_handler = \Drupal::moduleHandler();
+      $context = [
+        'fieldDefinition' => $this->fieldDefinition,
+        'entity' => $entity,
+      ];
+      $module_handler->alter('options_list', $options, $context);
+
+      array_walk_recursive($options, [$this, 'sanitizeLabel']);
+      $this->options = $options;
+    }
+    return $this->options;
   }
 
 }
diff --git a/web/modules/webform/src/Plugin/Field/FieldWidget/WebformEntityReferenceWidgetTrait.php b/web/modules/webform/src/Plugin/Field/FieldWidget/WebformEntityReferenceWidgetTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..7c9d5fc32a3d9215d781c0b308131f6b157eea89
--- /dev/null
+++ b/web/modules/webform/src/Plugin/Field/FieldWidget/WebformEntityReferenceWidgetTrait.php
@@ -0,0 +1,271 @@
+<?php
+
+namespace Drupal\webform\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Utility\WebformDateHelper;
+use Drupal\webform\WebformInterface;
+
+/**
+ * Trait for webform entity reference and autocomplete widget.
+ */
+trait WebformEntityReferenceWidgetTrait {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    return [
+      'default_data' => TRUE,
+    ] + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state) {
+    $element = parent::settingsForm($form, $form_state);
+    $element['default_data'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Enable default submission data (YAML)'),
+      '#description' => t('If checked, site builders will be able to define default submission data (YAML)'),
+      '#default_value' => $this->getSetting('default_data'),
+    ];
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = parent::settingsSummary();
+    $summary[] = t('Default submission data: @default_data', ['@default_data' => $this->getSetting('default_data') ? $this->t('Yes') : $this->t('No')]);
+    return $summary;
+  }
+
+  /**
+   * Returns the target id element form for a single webform field widget.
+   *
+   * @param \Drupal\Core\Field\FieldItemListInterface $items
+   *   Array of default values for this field.
+   * @param int $delta
+   *   The order of this item in the array of sub-elements (0, 1, 2, etc.).
+   * @param array $element
+   *   A form element array containing basic properties for the widget.
+   * @param array $form
+   *   The form structure where widgets are being attached to. This might be a
+   *   full form structure, or a sub-element of a larger form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   The form elements for a single widget for this field.
+   */
+  abstract protected function getTargetIdElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state);
+
+  /**
+   * {@inheritdoc}
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    // Set item default status to open.
+    if (!isset($items[$delta]->status)) {
+      $items[$delta]->status = WebformInterface::STATUS_OPEN;
+    }
+
+    // Get field name.
+    $field_name = $items->getName();
+
+    // Get field input name from field parents, field name, and the delta.
+    $field_parents = array_merge($element['#field_parents'], [$field_name, $delta]);
+    $field_input_name = (array_shift($field_parents)) . ('[' . implode('][', $field_parents) . ']');
+
+    // Get target ID element.
+    $target_id_element = $this->getTargetIdElement($items, $delta, $element, $form, $form_state);
+
+    // Determine if this is a paragraph.
+    $is_paragraph = ($items->getEntity()->getEntityTypeId() === 'paragraph');
+
+    // Merge target ID and default element and set default #weight.
+    // @see \Drupal\Core\Field\Plugin\Field\FieldWidget\EntityReferenceAutocompleteWidget::formElement
+    $element = [
+      'target_id' => $target_id_element + $element + ['#weight' => 0],
+    ];
+
+    // Get weight.
+    $weight = $element['target_id']['#weight'];
+
+    $element['settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('@title settings', ['@title' => $element['target_id']['#title']]),
+      '#element_validate' => [[$this, 'validateOpenClose']],
+      '#open' => ($items[$delta]->target_id) ? TRUE : FALSE,
+      '#weight' => $weight++,
+    ];
+
+    $element['settings']['status'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Status'),
+      '#options' => [
+        WebformInterface::STATUS_OPEN => $this->t('Open'),
+        WebformInterface::STATUS_CLOSED => $this->t('Closed'),
+        WebformInterface::STATUS_SCHEDULED => $this->t('Scheduled'),
+      ],
+      '#options_display' => 'side_by_side',
+      '#default_value' => $items[$delta]->status,
+    ];
+
+    $element['settings']['scheduled'] = [
+      '#type' => 'item',
+      '#title' => $element['target_id']['#title'],
+      '#title_display' => 'invisible',
+      '#input' => FALSE,
+      '#states' => [
+        'visible' => [
+          'input[name="' . $field_input_name . '[settings][status]"]' => ['value' => WebformInterface::STATUS_SCHEDULED],
+        ],
+      ],
+    ];
+    $element['settings']['scheduled']['open'] = [
+      '#type' => 'datetime',
+      '#title' => $this->t('Open'),
+      '#default_value' => $items[$delta]->open ? DrupalDateTime::createFromTimestamp(strtotime($items[$delta]->open)) : NULL,
+      '#prefix' => '<div class="container-inline form-item">',
+      '#suffix' => '</div>',
+      '#help' => FALSE,
+      '#description' => [
+        '#type' => 'webform_help',
+        '#help' => $this->t('If the open date/time is left blank, this form will immediately be opened.'),
+        '#help_title' => $this->t('Open'),
+      ],
+    ];
+    $element['settings']['scheduled']['close'] = [
+      '#type' => 'datetime',
+      '#title' => $this->t('Close'),
+      '#default_value' => $items[$delta]->close ? DrupalDateTime::createFromTimestamp(strtotime($items[$delta]->close)) : NULL,
+      '#prefix' => '<div class="container-inline form-item">',
+      '#suffix' => '</div>',
+      '#help' => FALSE,
+      '#description' => [
+        '#type' => 'webform_help',
+        '#help' => $this->t('If the close date/time is left blank, this webform will never be closed.'),
+        '#help_title' => $this->t('Close'),
+      ],
+    ];
+
+    if ($this->getSetting('default_data')) {
+      /** @var \Drupal\webform\WebformTokenManagerInterface $token_manager */
+      $token_manager = \Drupal::service('webform.token_manager');
+      $token_types = ['webform', 'webform_submission'];
+
+      $default_data_example = "# This is an example of a comment.
+element_key: 'some value'
+
+# The below example uses a token to get the current node's title.
+# Add ':clear' to the end token to return an empty value when the token is missing.
+title: '[webform_submission:node:title:clear]'
+# The below example uses a token to get a field value from the current node.
+full_name: '[webform_submission:node:field_full_name:clear]";
+      if ($is_paragraph) {
+        $token_types[] = 'paragraph';
+        $default_data_example .= PHP_EOL . "# You can also use paragraphs tokens.
+some_value: '[paragraph:some_value:clear]";
+      }
+      $element['settings']['default_data'] = [
+        '#type' => 'webform_codemirror',
+        '#mode' => 'yaml',
+        '#title' => $this->t('Default submission data (YAML)'),
+        '#placeholder' => $this->t("Enter 'name': 'value' pairs…"),
+        '#default_value' => $items[$delta]->default_data,
+        '#webform_element' => TRUE,
+        '#description' => [
+          'content' => ['#markup' => $this->t('Enter submission data as name and value pairs as <a href=":href">YAML</a> which will be used to prepopulate the selected webform.', [':href' => 'https://en.wikipedia.org/wiki/YAML']), '#suffix' => ' '],
+          'token' => $token_manager->buildTreeLink($token_types),
+        ],
+        '#more_title' => $this->t('Example'),
+        '#more' => [
+          '#theme' => 'webform_codemirror',
+          '#type' => 'yaml',
+          '#code' => $default_data_example,
+        ],
+      ];
+      $element['settings']['token_tree_link'] = $token_manager->buildTreeElement($token_types);
+      $token_manager->elementValidate($element['settings']['default_data'], $token_types);
+    }
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+    parent::massageFormValues($values, $form, $form_state);
+
+    // Massage open/close dates.
+    // @see \Drupal\webform\WebformEntitySettingsForm::save
+    // @see \Drupal\datetime\Plugin\Field\FieldWidget\DateTimeWidgetBase::massageFormValues
+    foreach ($values as &$item) {
+      $item += $item['settings'];
+      unset($item['settings']);
+
+      if ($item['status'] === WebformInterface::STATUS_SCHEDULED) {
+        $states = ['open', 'close'];
+        foreach ($states as $state) {
+          if (!empty($item['scheduled'][$state]) && $item['scheduled'][$state] instanceof DrupalDateTime) {
+            $item[$state] = WebformDateHelper::formatStorage($item['scheduled'][$state]);
+          }
+          else {
+            $item[$state] = '';
+          }
+        }
+      }
+      else {
+        $item['open'] = '';
+        $item['close'] = '';
+      }
+      unset($item['scheduled']);
+    }
+    return $values;
+  }
+
+  /**
+   * Validate callback to ensure that the open date <= the close date.
+   *
+   * @param array $element
+   *   An associative array containing the properties and children of the
+   *   generic form element.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   *
+   * @see \Drupal\webform\WebformEntitySettingsForm::validateForm
+   * @see \Drupal\datetime_range\Plugin\Field\FieldWidget\DateRangeWidgetBase::validateOpenClose
+   */
+  public function validateOpenClose(array &$element, FormStateInterface $form_state, array &$complete_form) {
+    $status = $element['status']['#value'];
+    if ($status === WebformInterface::STATUS_SCHEDULED) {
+      $open_date = $element['scheduled']['open']['#value']['object'];
+      $close_date = $element['scheduled']['close']['#value']['object'];
+
+      // Require open or close dates.
+      if (empty($open_date) && empty($close_date)) {
+        $form_state->setError($element['scheduled']['open'], $this->t('Please enter an open or close date'));
+        $form_state->setError($element['scheduled']['close'], $this->t('Please enter an open or close date'));
+      }
+
+      // Make sure open date is not after close date.
+      if ($open_date instanceof DrupalDateTime && $close_date instanceof DrupalDateTime) {
+        if ($open_date->getTimestamp() !== $close_date->getTimestamp()) {
+          $interval = $open_date->diff($close_date);
+          if ($interval->invert === 1) {
+            $form_state->setError($element['scheduled']['open'], $this->t('The @title close date cannot be before the open date', ['@title' => $element['#title']]));
+          }
+        }
+      }
+    }
+  }
+
+}
diff --git a/web/modules/webform/src/Plugin/Mail/WebformPhpMail.php b/web/modules/webform/src/Plugin/Mail/WebformPhpMail.php
index 53ce53532550c87db38f366a84566e6d296b086a..7fc6ec546925e3e9e01d75974586ae1dd54b6634 100644
--- a/web/modules/webform/src/Plugin/Mail/WebformPhpMail.php
+++ b/web/modules/webform/src/Plugin/Mail/WebformPhpMail.php
@@ -24,6 +24,19 @@ public function format(array $message) {
     $message['body'] = implode("\n\n", $message['body']);
 
     if (!empty($message['params']['html'])) {
+      // Wrap body in HTML template if the <html> tag is missing.
+      if (strpos($message['body'], '<html') === FALSE) {
+        // Make sure parameters exist.
+        $message['params'] += ['webform_submission' => NULL, 'handler' => NULL];
+        $build = [
+          '#theme' => 'webform_email_html',
+          '#body' => $message['body'],
+          '#subject' => $message['subject'],
+          '#webform_submission' => $message['params']['webform_submission'],
+          '#handler' => $message['params']['handler'],
+        ];
+        $message['body'] = \Drupal::service('renderer')->renderPlain($build);
+      }
       return $message;
     }
     else {
diff --git a/web/modules/webform/src/Plugin/Menu/LocalAction/WebformDialogLocalAction.php b/web/modules/webform/src/Plugin/Menu/LocalAction/WebformDialogLocalAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..9ddcda0712af09c76a8d87db09e23fc23c345c65
--- /dev/null
+++ b/web/modules/webform/src/Plugin/Menu/LocalAction/WebformDialogLocalAction.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\webform\Plugin\Menu\LocalAction;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Menu\LocalActionDefault;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\webform\Utility\WebformDialogHelper;
+
+/**
+ * Defines a local action plugin with the needed dialog attributes.
+ */
+class WebformDialogLocalAction extends LocalActionDefault {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOptions(RouteMatchInterface $route_match) {
+    $options = parent::getOptions($route_match);
+
+    if (isset($this->pluginDefinition['dialog'])) {
+      $attributes = WebformDialogHelper::getModalDialogAttributes($this->pluginDefinition['dialog']);
+    }
+    elseif (isset($this->pluginDefinition['off_canvas'])) {
+      $attributes = WebformDialogHelper::getOffCanvasDialogAttributes($this->pluginDefinition['off_canvas']);
+    }
+    else {
+      $attributes = [];
+    }
+
+    $options['attributes'] = (isset($this->pluginDefinition['attributes'])) ? $this->pluginDefinition['attributes'] : [];
+    $options['attributes'] = NestedArray::mergeDeep($options['attributes'], $attributes);
+
+    return $options;
+  }
+
+}
diff --git a/web/modules/webform/src/Plugin/WebformElement/Address.php b/web/modules/webform/src/Plugin/WebformElement/Address.php
new file mode 100644
index 0000000000000000000000000000000000000000..245eaa86e089d1213b4373869a6200263ed52e3a
--- /dev/null
+++ b/web/modules/webform/src/Plugin/WebformElement/Address.php
@@ -0,0 +1,429 @@
+<?php
+
+namespace Drupal\webform\Plugin\WebformElement;
+
+use CommerceGuys\Addressing\AddressFormat\FieldOverride;
+use Drupal\address\FieldHelper;
+use Drupal\address\LabelHelper;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Mail\MailFormatHelper;
+use Drupal\webform\Utility\WebformElementHelper;
+use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformSubmissionInterface;
+
+/**
+ * Provides a 'address' element.
+ *
+ * @WebformElement(
+ *   id = "address",
+ *   label = @Translation("Advanced address"),
+ *   description = @Translation("Provides advanced element for storing, validating and displaying international postal addresses."),
+ *   category = @Translation("Composite elements"),
+ *   composite = TRUE,
+ *   multiline = TRUE,
+ *   states_wrapper = TRUE,
+ *   dependencies = {
+ *     "address",
+ *   }
+ * )
+ *
+ * @see \Drupal\address\Element\Address
+ */
+class Address extends WebformCompositeBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultProperties() {
+    $properties = [
+      // Element settings.
+      'title' => '',
+      'default_value' => [],
+      // Description/Help.
+      'help' => '',
+      'help_title' => '',
+      'description' => '',
+      'more' => '',
+      'more_title' => '',
+      // Form display.
+      'title_display' => 'invisible',
+      'description_display' => '',
+      // Form validation.
+      'required' => FALSE,
+      // Submission display.
+      'format' => $this->getItemDefaultFormat(),
+      'format_html' => '',
+      'format_text' => '',
+      'format_items' => $this->getItemsDefaultFormat(),
+      'format_items_html' => '',
+      'format_items_text' => '',
+      // Address settings.
+      'available_countries' => [],
+      'field_overrides' => [],
+      'langcode_override' => '',
+    ] + $this->getDefaultBaseProperties() + $this->getDefaultMultipleProperties();
+    unset($properties['multiple__header']);
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    parent::prepare($element, $webform_submission);
+
+    $element['#theme_wrappers'] = [];
+
+    // #title display defaults to invisible.
+    $element += [
+      '#title_display' => 'invisible',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareElementValidateCallbacks(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    parent::prepareElementValidateCallbacks($element, $webform_submission);
+
+    $element['#element_validate'][] = [get_class($this), 'validateAddress'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareElementPreRenderCallbacks(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    parent::prepareElementPreRenderCallbacks($element, $webform_submission);
+
+    // Replace 'form_element' theme wrapper with composite form element.
+    // @see \Drupal\Core\Render\Element\PasswordConfirm
+    $element['#pre_render'] = [[get_called_class(), 'preRenderWebformCompositeFormElement']];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCompositeElements() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @see \Drupal\address\Plugin\Field\FieldType\AddressItem::schema
+   */
+  public function initializeCompositeElements(array &$element) {
+    $element['#webform_composite_elements'] = [
+      'given_name' => [
+        '#title' => $this->t('Given name'),
+        '#type' => 'textfield',
+        '#maxlength' => 255,
+      ],
+      'family_name' => [
+        '#title' => $this->t('Family name'),
+        '#type' => 'textfield',
+        '#maxlength' => 255,
+      ],
+      'additional_name' => [
+        '#title' => $this->t('Additional name'),
+        '#type' => 'textfield',
+        '#maxlength' => 255,
+      ],
+      'organization' => [
+        '#title' => $this->t('Organization'),
+        '#type' => 'textfield',
+        '#maxlength' => 255,
+      ],
+      'address_line1' => [
+        '#title' => $this->t('Address line 1'),
+        '#type' => 'textfield',
+        '#maxlength' => 255,
+      ],
+      'address_line2' => [
+        '#title' => $this->t('Address line 2'),
+        '#type' => 'textfield',
+        '#maxlength' => 255,
+      ],
+      'postal_code' => [
+        '#title' => $this->t('Postal code'),
+        '#type' => 'textfield',
+        '#maxlength' => 255,
+      ],
+      'locality' => [
+        '#title' => $this->t('Locality'),
+        '#type' => 'textfield',
+        '#maxlength' => 255,
+      ],
+      'dependent_locality' => [
+        '#title' => $this->t('Dependent_locality'),
+        '#type' => 'textfield',
+        '#maxlength' => 255,
+      ],
+      'administrative_area' => [
+        '#title' => $this->t('Administrative area'),
+        '#type' => 'textfield',
+        '#maxlength' => 255,
+      ],
+      'country_code' => [
+        '#title' => $this->t('Country code'),
+        '#type' => 'textfield',
+        '#maxlength' => 2,
+      ],
+      'langcode' => [
+        '#title' => $this->t('Language code'),
+        '#type' => 'textfield',
+        '#maxlength' => 32,
+      ],
+      'sorting_code' => [
+        '#title' => $this->t('Sorting code'),
+        '#type' => 'textfield',
+        '#maxlength' => 255,
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function formatHtmlItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
+    $format = $this->getItemFormat($element);
+    if ($format === 'value') {
+      return $this->buildAddress($element, $webform_submission, $options);
+    }
+    else {
+      return parent::formatHtmlItem($element, $webform_submission, $options);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function formatTextItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
+    $format = $this->getItemFormat($element);
+    if ($format === 'value') {
+      $build = $this->buildAddress($element, $webform_submission, $options);
+      $html = \Drupal::service('renderer')->renderPlain($build);
+      return trim(MailFormatHelper::htmlToText($html));
+    }
+    else {
+      return parent::formatTextItem($element, $webform_submission, $options);
+    }
+  }
+
+  /**
+   * Build formatted address.
+   *
+   * The below code is copied form the protected
+   * AddressDefaultFormatter::viewElements method.
+   *
+   * @param array $element
+   *   An element.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   * @param array $options
+   *   An array of options.
+   *
+   * @return array
+   *   A render array containing the formatted address.
+   *
+   * @see \Drupal\address\Plugin\Field\FieldFormatter\AddressDefaultFormatter::viewElements
+   * @see \Drupal\address\Plugin\Field\FieldFormatter\AddressDefaultFormatter::viewElement
+   */
+  protected function buildAddress(array $element, WebformSubmissionInterface $webform_submission, array $options) {
+    /** @var \CommerceGuys\Addressing\AddressFormat\AddressFormatRepositoryInterface $address_format_repository */
+    $address_format_repository = \Drupal::service('address.address_format_repository');
+    /** @var \CommerceGuys\Addressing\Country\CountryRepositoryInterface $country_repository */
+    $country_repository = \Drupal::service('address.country_repository');
+
+    $value = $this->getValue($element, $webform_submission, $options);
+    // Skip if value or country code is empty.
+    if (empty($value) || empty($value['country_code'])) {
+      return [];
+    }
+
+    // @see \Drupal\address\Plugin\Field\FieldFormatter\AddressDefaultFormatter::viewElements
+    $build = [
+      '#prefix' => '<div class="address" translate="no">',
+      '#suffix' => '</div>',
+      '#post_render' => [
+        ['\Drupal\address\Plugin\Field\FieldFormatter\AddressDefaultFormatter', 'postRender'],
+      ],
+      '#cache' => [
+        'contexts' => [
+          'languages:' . LanguageInterface::TYPE_INTERFACE,
+        ],
+      ],
+    ];
+
+    // @see \Drupal\address\Plugin\Field\FieldFormatter\AddressDefaultFormatter::viewElement
+    $country_code = $value['country_code'];
+    $countries = $country_repository->getList();
+    $address_format = $address_format_repository->get($country_code);
+
+    $build += [
+      '#address_format' => $address_format,
+      '#locale' => 'und',
+    ];
+    $build['country'] = [
+      '#type' => 'html_tag',
+      '#tag' => 'span',
+      '#attributes' => ['class' => ['country']],
+      '#value' => Html::escape($countries[$country_code]),
+      '#placeholder' => '%country',
+    ];
+    foreach ($address_format->getUsedFields() as $field) {
+      $property = FieldHelper::getPropertyName($field);
+      $class = str_replace('_', '-', $property);
+      $build[$property] = [
+        '#type' => 'html_tag',
+        '#tag' => 'span',
+        '#attributes' => ['class' => [$class]],
+        '#value' => (!empty($value[$property])) ? Html::escape($value[$property]) : '',
+        '#placeholder' => '%' . $field,
+      ];
+    }
+
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+
+    // Get field overrider from element properties.
+    $element_properties = $form_state->get('element_properties');
+    $field_overrides = $element_properties['field_overrides'];
+    unset($element_properties['field_overrides']);
+    $form_state->set('element_properties', $element_properties);
+
+    $form['address'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Address settings'),
+    ];
+
+    /**************************************************************************/
+    // Copied from: \Drupal\address\Plugin\Field\FieldType\AddressItem::fieldSettingsForm
+    /**************************************************************************/
+
+    $languages = \Drupal::languageManager()->getLanguages(LanguageInterface::STATE_ALL);
+    $language_options = [];
+    foreach ($languages as $langcode => $language) {
+      if (!$language->isLocked()) {
+        $language_options[$langcode] = $language->getName();
+      }
+    }
+
+    $form['address']['available_countries'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Available countries'),
+      '#description' => $this->t('If no countries are selected, all countries will be available.'),
+      '#options' => \Drupal::service('address.country_repository')->getList(),
+      '#multiple' => TRUE,
+      '#size' => 10,
+      '#select2' => TRUE,
+    ];
+    WebformElementHelper::process($form['address']['available_countries']);
+    $form['address']['langcode_override'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Language override'),
+      '#description' => $this->t('Ensures entered addresses are always formatted in the same language.'),
+      '#options' => $language_options,
+      '#empty_option' => $this->t('- No override -'),
+      '#access' => \Drupal::languageManager()->isMultilingual(),
+    ];
+    $form['address']['field_overrides_title'] = [
+      '#type' => 'item',
+      '#title' => $this->t('Field overrides'),
+      '#description' => $this->t('Use field overrides to override the country-specific address format, forcing specific fields to always be hidden, optional, or required.'),
+      '#access' => TRUE,
+    ];
+    $form['address']['field_overrides'] = [
+      '#type' => 'table',
+      '#header' => [
+        $this->t('Field'),
+        $this->t('Override'),
+      ],
+      '#access' => TRUE,
+    ];
+    foreach (LabelHelper::getGenericFieldLabels() as $field_name => $label) {
+      $override = isset($field_overrides[$field_name]) ? $field_overrides[$field_name] : '';
+      $form['address']['field_overrides'][$field_name] = [
+        '#access' => TRUE,
+        'field_label' => [
+          '#access' => TRUE,
+          '#type' => 'markup',
+          '#markup' => $label,
+        ],
+        'override' => [
+          '#access' => TRUE,
+          '#type' => 'select',
+          '#default_value' => $override,
+          '#options' => [
+            FieldOverride::HIDDEN => t('Hidden'),
+            FieldOverride::OPTIONAL => t('Optional'),
+            FieldOverride::REQUIRED => t('Required'),
+          ],
+          '#empty_option' => $this->t('- No override -'),
+          '#parents' => ['properties', 'field_overrides', $field_name],
+        ],
+      ];
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $values = $form_state->getValues();
+    $values['field_overrides'] = array_filter($values['field_overrides']);
+    $form_state->setValues($values);
+    parent::validateConfigurationForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTestValues(array $element, WebformInterface $webform, array $options = []) {
+    return [
+      [
+        'given_name' => 'John',
+        'family_name' => 'Smith',
+        'organization' => 'Google Inc.',
+        'address_line1' => '1098 Alta Ave',
+        'postal_code' => '94043',
+        'locality' => 'Mountain View',
+        'administrative_area' => 'CA',
+        'country_code' => 'US',
+        'langcode' => 'en',
+      ],
+    ];
+  }
+
+  /**
+   * Form API callback. Make sure address element value includes a country code.
+   */
+  public static function validateAddress(array &$element, FormStateInterface $form_state, array &$completed_form) {
+    $value = $element['#value'];
+    if (empty($element['#multiple'])) {
+      if (empty($value['country_code'])) {
+        $form_state->setValueForElement($element, NULL);
+      }
+    }
+    else {
+      foreach ($value as $index => $item) {
+        if (empty($item['country_code'])) {
+          unset($value[$index]);
+        }
+      }
+      $value = array_values($value);
+      $form_state->setValueForElement($element, $value ?: NULL);
+    }
+  }
+
+}
diff --git a/web/modules/webform/src/Plugin/WebformElement/Captcha.php b/web/modules/webform/src/Plugin/WebformElement/Captcha.php
index 8a681493242aaf3cce4c8b94e8a5c19c112a38e9..0afb5105275a18280cdb78e15f1a745878ce5b2c 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Captcha.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Captcha.php
@@ -86,7 +86,6 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
     parent::prepare($element, $webform_submission);
 
     $element['#after_build'][] = [get_class($this), 'afterBuildCaptcha'];
-
   }
 
   /**
diff --git a/web/modules/webform/src/Plugin/WebformElement/Checkbox.php b/web/modules/webform/src/Plugin/WebformElement/Checkbox.php
index 1e3c5d67055175e92181004771c806c013e96c92..967b3726c0804b8244e33b4cbb37a05776d58ebc 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Checkbox.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Checkbox.php
@@ -2,6 +2,9 @@
 
 namespace Drupal\webform\Plugin\WebformElement;
 
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\WebformSubmissionInterface;
+
 /**
  * Provides a 'checkbox' element.
  *
@@ -21,6 +24,8 @@ class Checkbox extends BooleanBase {
   public function getDefaultProperties() {
     $properties = [
       'title_display' => 'after',
+      // Checkbox.
+      'exclude_empty' => FALSE,
       // iCheck settings.
       'icheck' => '',
     ] + parent::getDefaultProperties();
@@ -28,4 +33,42 @@ public function getDefaultProperties() {
     return $properties;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function build($format, array &$element, WebformSubmissionInterface $webform_submission, array $options = []) {
+    if ($this->isEmptyExcluded($element, $options) && !$this->getValue($element, $webform_submission, $options)) {
+      return NULL;
+    }
+    else {
+      return parent::build($format, $element, $webform_submission, $options);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmptyExcluded(array $element, array $options) {
+    $options += [
+      'exclude_empty_checkbox' => FALSE,
+    ];
+
+    return $this->getElementProperty($element, 'exclude_empty') ?: $options['exclude_empty_checkbox'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+
+    $form['display']['exclude_empty'] = [
+      '#title' => $this->t('Exclude unselected checkbox'),
+      '#type' => 'checkbox',
+      '#return_value' => TRUE,
+    ];
+
+    return $form;
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/Checkboxes.php b/web/modules/webform/src/Plugin/WebformElement/Checkboxes.php
index 4d3ce2fd9e548b975d44f1274f3fedc77d734d5e..2d7ce2a0992f7b53b0f3438db30956451ff17306 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Checkboxes.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Checkboxes.php
@@ -3,6 +3,7 @@
 namespace Drupal\webform\Plugin\WebformElement;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Utility\WebformOptionsHelper;
 use Drupal\webform\WebformSubmissionConditionsValidator;
 use Drupal\webform\WebformSubmissionInterface;
 
@@ -31,6 +32,8 @@ public function getDefaultProperties() {
       'options_description_display' => 'description',
       // iCheck settings.
       'icheck' => '',
+      // Wrapper.
+      'wrapper_type' => 'fieldset',
     ] + parent::getDefaultProperties();
   }
 
@@ -48,21 +51,17 @@ public function hasMultipleValues(array $element) {
     return TRUE;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
-    $element['#element_validate'][] = [get_class($this), 'validateMultipleOptions'];
-    parent::prepare($element, $webform_submission);
-  }
-
   /**
    * {@inheritdoc}
    */
   protected function getElementSelectorInputsOptions(array $element) {
     $selectors = $element['#options'];
-    foreach ($selectors as &$text) {
+    foreach ($selectors as $index => $text) {
+      // Remove description from text.
+      list($text) = explode(WebformOptionsHelper::DESCRIPTION_DELIMITER, $text);
+      // Append element type to text.
       $text .= ' [' . $this->t('Checkbox') . ']';
+      $selectors[$index] = $text;
     }
     return $selectors;
   }
@@ -82,6 +81,13 @@ public function getElementSelectorInputValue($selector, $trigger, array $element
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorSourceValues(array $element) {
+    return [];
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/web/modules/webform/src/Plugin/WebformElement/Container.php b/web/modules/webform/src/Plugin/WebformElement/Container.php
index 4f142a94328076633292499457c2cf06aea65304..2ff27f5ccd97271f3eb162c72010ace28741a136 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Container.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Container.php
@@ -23,14 +23,18 @@ public function getDefaultProperties() {
     return [
       // Attributes.
       'attributes' => [],
+      // Randomize.
+      'randomize' => FALSE,
       // Flexbox.
       'flex' => 1,
       // Conditional logic.
       'states' => [],
+      'states_clear' => TRUE,
       // Format.
       'format' => $this->getItemDefaultFormat(),
       'format_html' => '',
       'format_text' => '',
+      'format_attributes' => [],
     ];
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/ContainerBase.php b/web/modules/webform/src/Plugin/WebformElement/ContainerBase.php
index abf75034ce2ae1a2dd9be75d55ac3a84d0f240ce..77d25815b552d05763a29eb336091359078fecf0 100644
--- a/web/modules/webform/src/Plugin/WebformElement/ContainerBase.php
+++ b/web/modules/webform/src/Plugin/WebformElement/ContainerBase.php
@@ -2,9 +2,10 @@
 
 namespace Drupal\webform\Plugin\WebformElement;
 
-use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
 use Drupal\webform\Plugin\WebformElementBase;
+use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformSubmissionInterface;
 
@@ -19,16 +20,17 @@ abstract class ContainerBase extends WebformElementBase {
   public function getDefaultProperties() {
     return [
       'title' => '',
-      // General settings.
-      'description' => '',
       // Form validation.
       'required' => FALSE,
+      // Randomize.
+      'randomize' => FALSE,
       // Attributes.
       'attributes' => [],
       // Format.
       'format' => $this->getItemDefaultFormat(),
       'format_html' => '',
       'format_text' => '',
+      'format_attributes' => [],
     ] + $this->getDefaultBaseProperties();
   }
 
@@ -61,14 +63,13 @@ public function isContainer(array $element) {
   public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
     parent::prepare($element, $webform_submission);
 
-    // Containers can only hide (aka invisible) the title by removing the
-    // #title attribute.
-    // @see core/modules/system/templates/fieldset.html.twig
-    // @see core/modules/system/templates/details.html.twig
-    if (isset($element['#title_display'])
-      && $element['#title_display'] === 'invisible'
-      && ($this instanceof Fieldset || $this instanceof Details)) {
-      unset($element['#title']);
+    if (!empty($element['#randomize'])) {
+      $elements = [];
+      foreach (Element::children($element) as $child_key) {
+        $elements[$child_key] = $element[$child_key];
+        unset($element[$child_key]);
+      }
+      $element += WebformElementHelper::randomize($elements);
     }
   }
 
@@ -128,35 +129,34 @@ protected function formatHtmlItem(array $element, WebformSubmissionInterface $we
       $format = 'header';
     }
 
+    // Build format attributes.
+    $attributes = (isset($element['#format_attributes'])) ? $element['#format_attributes'] : [];
+    $attributes += ['class' => []];
+
     switch ($format) {
       case 'details':
       case 'details-closed':
+        $attributes['data-webform-element-id'] = $element['#webform_id'];
+        $attributes['class'][] = 'webform-container';
+        $attributes['class'][] = 'webform-container-type-details';
         return [
           '#type' => 'details',
           '#title' => $element['#title'],
           '#id' => $element['#webform_id'],
           '#open' => ($format === 'details-closed') ? FALSE : TRUE,
-          '#attributes' => [
-            'data-webform-element-id' => $element['#webform_id'],
-            'class' => [
-              'webform-container',
-              'webform-container-type-details',
-            ],
-          ],
+          '#attributes' => $attributes,
           '#children' => $children,
         ];
 
       case 'fieldset':
+        $attributes['class'][] = 'webform-container';
+        $attributes['class'][] = 'webform-container-type-fieldset';
+
         return [
           '#type' => 'fieldset',
           '#title' => $element['#title'],
           '#id' => $element['#webform_id'],
-          '#attributes' => [
-            'class' => [
-              'webform-container',
-              'webform-container-type-fieldset',
-            ],
-          ],
+          '#attributes' => $attributes,
           '#children' => $children,
         ];
 
@@ -167,6 +167,7 @@ protected function formatHtmlItem(array $element, WebformSubmissionInterface $we
           '#id' => $element['#webform_id'],
           '#title' => $element['#title'],
           '#title_tag' => \Drupal::config('webform.settings')->get('element.default_section_title_tag'),
+          '#attributes' => $attributes,
         ] + $children;
     }
   }
@@ -189,18 +190,18 @@ protected function formatTextItem(array $element, WebformSubmissionInterface $we
         '#suffix' => PHP_EOL,
       ];
       $build['divider'] = [
-        '#markup' => str_repeat('-', Unicode::strlen($element['#title'])),
+        '#markup' => str_repeat('-', mb_strlen($element['#title'])),
         '#suffix' => PHP_EOL,
       ];
     }
-    $build += $children;
+    $build['children'] = $children;
     return $build;
   }
 
   /**
    * {@inheritdoc}
    */
-  protected function formatCustomItem($type, array &$element, WebformSubmissionInterface $webform_submission, array $options = []) {
+  protected function formatCustomItem($type, array &$element, WebformSubmissionInterface $webform_submission, array $options = [], array $context = []) {
     $name = strtolower($type);
 
     // Parse children from template and children to context.
@@ -208,12 +209,10 @@ protected function formatCustomItem($type, array &$element, WebformSubmissionInt
     if (strpos($template, 'children') != FALSE) {
       /** @var \Drupal\webform\WebformSubmissionViewBuilderInterface $view_builder */
       $view_builder = \Drupal::entityTypeManager()->getViewBuilder('webform_submission');
-      $options['context'] = [
-        'children' => $view_builder->buildElements($element, $webform_submission, $options, $name),
-      ];
+      $context['children'] = $view_builder->buildElements($element, $webform_submission, $options, $name);
     }
 
-    return parent::formatCustomItem($type, $element, $webform_submission, $options);
+    return parent::formatCustomItem($type, $element, $webform_submission, $options, $context);
   }
 
   /**
@@ -240,6 +239,15 @@ public function getItemFormats() {
    */
   public function form(array $form, FormStateInterface $form_state) {
     $form = parent::form($form, $form_state);
+
+    // Randomize.
+    $form['element']['randomize'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Randomize elements'),
+      '#description' => $this->t('Randomizes the order of the sub-element when they are displayed in the webform.'),
+      '#return_value' => TRUE,
+    ];
+
     // Containers are wrappers, therefore wrapper classes should be used by the
     // container element.
     $form['element_attributes']['attributes']['#classes'] = $this->configFactory->get('webform.settings')->get('element.wrapper_classes');
@@ -251,10 +259,6 @@ public function form(array $form, FormStateInterface $form_state) {
       'invisible' => $this->t('Invisible'),
     ];
 
-    // Remove value from item custom display replacement patterns.
-    $item_patterns = &$form['display']['item']['patterns']['#value']['items']['#items'];
-    unset($item_patterns['value']);
-    $item_patterns = ['children' => '{{ children }}'] + $item_patterns;
     return $form;
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/Date.php b/web/modules/webform/src/Plugin/WebformElement/Date.php
index 43a43618511081da9ccd5addea974397cb181ec1..d46278b39719501f3d36af9a7017deef478728fc 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Date.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Date.php
@@ -35,6 +35,7 @@ public function getDefaultProperties() {
     return [
       // Date settings.
       'datepicker' => FALSE,
+      'datepicker_button' => FALSE,
       'date_date_format' => $date_format,
       'step' => '',
       'size' => '',
@@ -59,8 +60,7 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
     // Prepare element after date format has been updated.
     parent::prepare($element, $webform_submission);
 
-    // Set the (input) type attribute to 'date' since #min and #max will
-    // override the default attributes.
+    // Set the (input) type attribute to 'date'.
     // @see \Drupal\Core\Render\Element\Date::getInfo
     $element['#attributes']['type'] = 'date';
 
@@ -71,16 +71,25 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       // Must manually set 'data-drupal-date-format' to trigger date picker.
       // @see \Drupal\Core\Render\Element\Date::processDate
       $element['#attributes']['data-drupal-date-format'] = [$element['#date_date_format']];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setDefaultValue(array &$element) {
+    parent::setDefaultValue($element);
 
-      // Format default value.
+    // Format date picker default value.
+    if (!empty($element['#datepicker'])) {
       if (isset($element['#default_value'])) {
         if ($this->hasMultipleValues($element)) {
           foreach ($element['#default_value'] as $index => $default_value) {
-            $element['#default_value'][$index] = date($element['#date_date_format'], strtotime($default_value));
+            $element['#default_value'][$index] = static::formatDate($element['#date_date_format'], strtotime($default_value));
           }
         }
         else {
-          $element['#default_value'] = date($element['#date_date_format'], strtotime($element['#default_value']));
+          $element['#default_value'] = static::formatDate($element['#date_date_format'], strtotime($element['#default_value']));
         }
       }
     }
@@ -111,19 +120,30 @@ public function form(array $form, FormStateInterface $form_state) {
       '#description' => $this->t('If checked, the HTML5 date element will be replaced with <a href="https://jqueryui.com/datepicker/">jQuery UI datepicker</a>'),
       '#return_value' => TRUE,
     ];
+    $form['date']['datepicker_button'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Show date picker button'),
+      '#description' => $this->t('If checked, date picker will include a calendar button'),
+      '#return_value' => TRUE,
+      '#states' => [
+        'visible' => [
+          ':input[name="properties[datepicker]"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
     $date_format = DateFormat::load('html_date')->getPattern();
     $form['date']['date_date_format'] = [
       '#type' => 'webform_select_other',
       '#title' => $this->t('Date format'),
       '#options' => [
-        $date_format => $this->t('HTML date - @format (@date)', ['@format' => $date_format, '@date' => date($date_format)]),
-        'l, F j, Y' => $this->t('Long date - @format (@date)', ['@format' => 'l, F j, Y', '@date' => date('l, F j, Y')]),
-        'D, m/d/Y' => $this->t('Medium date - @format (@date)', ['@format' => 'D, m/d/Y', '@date' => date('D, m/d/Y')]),
-        'm/d/Y' => $this->t('Short date - @format (@date)', ['@format' => 'm/d/Y', '@date' => date('m/d/Y')]),
+        $date_format => $this->t('HTML date - @format (@date)', ['@format' => $date_format, '@date' => static::formatDate($date_format)]),
+        'l, F j, Y' => $this->t('Long date - @format (@date)', ['@format' => 'l, F j, Y', '@date' => static::formatDate('l, F j, Y')]),
+        'D, m/d/Y' => $this->t('Medium date - @format (@date)', ['@format' => 'D, m/d/Y', '@date' => static::formatDate('D, m/d/Y')]),
+        'm/d/Y' => $this->t('Short date - @format (@date)', ['@format' => 'm/d/Y', '@date' => static::formatDate('m/d/Y')]),
       ],
       '#description' => $this->t("Date format is only applicable for browsers that do not have support for the HTML5 date element. Browsers that support the HTML5 date element will display the date using the user's preferred format."),
-      '#other__option_label' => $this->t('Custom...'),
-      '#other__placeholder' => $this->t('Custom date format...'),
+      '#other__option_label' => $this->t('Custom…'),
+      '#other__placeholder' => $this->t('Custom date format…'),
       '#other__description' => $this->t('Enter date format using <a href="http://php.net/manual/en/function.date.php">Date Input Format</a>.'),
       '#states' => [
         'visible' => [
diff --git a/web/modules/webform/src/Plugin/WebformElement/DateBase.php b/web/modules/webform/src/Plugin/WebformElement/DateBase.php
index 4e42236e86706196d37ba80d0d8afebe8de7f2d8..2330ca7c019930700f9b5ed17ae71069bb1fd6ff 100644
--- a/web/modules/webform/src/Plugin/WebformElement/DateBase.php
+++ b/web/modules/webform/src/Plugin/WebformElement/DateBase.php
@@ -8,7 +8,10 @@
 use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Datetime\Entity\DateFormat;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+use Drupal\webform\Element\WebformMessage as WebformMessageElement;
 use Drupal\webform\Plugin\WebformElementBase;
+use Drupal\webform\Utility\WebformDateHelper;
 use Drupal\webform\WebformSubmissionInterface;
 use Drupal\webform\WebformInterface;
 
@@ -23,8 +26,8 @@ abstract class DateBase extends WebformElementBase {
   public function getDefaultProperties() {
     return [
       // Form validation.
-      'min' => '',
-      'max' => '',
+      'date_date_min' => '',
+      'date_date_max' => '',
     ] + parent::getDefaultProperties() + $this->getDefaultMultipleProperties();
   }
 
@@ -54,36 +57,53 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
     // Parse #default_value date input format.
     $this->parseInputFormat($element, '#default_value');
 
-    // Override min/max attributes.
+    // Set date min/max attributes.
+    // This overrides extra attributes set via Datetime::processDatetime.
+    // @see \Drupal\Core\Datetime\Element\Datetime::processDatetime
     if (isset($element['#date_date_format'])) {
-      if (!empty($element['#min'])) {
-        $element['#attributes']['min'] = date($element['#date_date_format'], strtotime($element['#min']));
-        $element['#attributes']['data-min-year'] = date('Y', strtotime($element['#min']));
+      $date_min = $this->getElementProperty($element, 'date_date_min') ?: $this->getElementProperty($element, 'date_min');
+      if ($date_min) {
+        $element['#attributes']['min'] = static::formatDate($element['#date_date_format'], strtotime($date_min));
+        $element['#attributes']['data-min-year'] = static::formatDate('Y', strtotime($date_min));
       }
-      if (!empty($element['#max'])) {
-        $element['#attributes']['max'] = date($element['#date_date_format'], strtotime($element['#max']));
-        $element['#attributes']['data-max-year'] = date('Y', strtotime($element['#max']));
+      $date_max = $this->getElementProperty($element, 'date_date_max') ?: $this->getElementProperty($element, 'date_max');
+      if (!empty($date_max)) {
+        $element['#attributes']['max'] = static::formatDate($element['#date_date_format'], strtotime($date_max));
+        $element['#attributes']['data-max-year'] = static::formatDate('Y', strtotime($date_max));
       }
     }
 
-    $element['#element_validate'] = array_merge([[get_class($this), 'preValidateDate']], $element['#element_validate']);
-    $element['#element_validate'][] = [get_class($this), 'validateDate'];
+    // Display datepicker button.
+    if (!empty($element['#datepicker_button']) || !empty($element['#date_date_datepicker_button'])) {
+      $element['#attributes']['data-datepicker-button'] = TRUE;
+      $element['#attached']['drupalSettings']['webform']['datePicker']['buttonImage'] = base_path() . drupal_get_path('module', 'webform') . '/images/elements/date-calendar.png';
+    }
 
     // Set first day according to admin/config/regional/settings.
     $config = $this->configFactory->get('system.date');
     $element['#attached']['drupalSettings']['webform']['dateFirstDay'] = $config->get('first_day');
-
     $cacheability = CacheableMetadata::createFromObject($config);
     $cacheability->applyTo($element);
 
     $element['#attached']['library'][] = 'webform/webform.element.date';
+
+    $element['#after_build'][] = [get_class($this), 'afterBuild'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareElementValidateCallbacks(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    $element['#element_validate'] = array_merge([[get_class($this), 'preValidateDate']], $element['#element_validate']);
+    $element['#element_validate'][] = [get_class($this), 'validateDate'];
+    parent::prepareElementValidateCallbacks($element, $webform_submission);
   }
 
   /**
    * {@inheritdoc}
    */
   public function setDefaultValue(array &$element) {
-    if (!empty($element['#multiple'])) {
+    if ($this->hasMultipleValues($element)) {
       $element['#default_value'] = (isset($element['#default_value'])) ? (array) $element['#default_value'] : NULL;
       return;
     }
@@ -96,6 +116,31 @@ public function setDefaultValue(array &$element) {
     }
   }
 
+  /**
+   * After build handler for date elements.
+   */
+  public static function afterBuild(array $element, FormStateInterface $form_state) {
+    // Add parent title to sub-elements to child elements which applies to
+    // datetime and datelist elements.
+    $child_keys = Element::children($element);
+    foreach ($child_keys as $child_key) {
+      if (isset($element[$child_key]['#title'])) {
+        $t_args = [
+          '@parent' => $element['#title'],
+          '@child' => $element[$child_key]['#title'],
+        ];
+        $element[$child_key]['#title'] = t('@parent: @child', $t_args);
+      }
+    }
+
+    // Remove orphaned form label.
+    if ($child_keys) {
+      $element['#label_attributes']['webform-remove-for-attribute'] = TRUE;
+    }
+
+    return $element;
+  }
+
   /****************************************************************************/
   // Display submission value methods.
   /****************************************************************************/
@@ -119,7 +164,7 @@ protected function formatTextItem(array $element, WebformSubmissionInterface $we
       return \Drupal::service('date.formatter')->format($timestamp, $format);
     }
     else {
-      return date($format, $timestamp);
+      return static::formatDate($format, $timestamp);
     }
   }
 
@@ -142,7 +187,7 @@ public function getItemFormats() {
     // If a default format is defined update the fallback date formats label.
     // @see \Drupal\webform\Plugin\WebformElementBase::getItemFormat
     $default_format = $this->configFactory->get('webform.settings')->get('format.' . $this->getPluginId() . '.item');
-    if ($default_format && isset($formats[$default_format])) {
+    if ($default_format && isset($date_formats[$default_format])) {
       $formats['fallback'] = t('Default date format (@label)', ['@label' => $date_formats[$default_format]->label()]);
     }
     return $formats;
@@ -174,11 +219,11 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['default']['default_value']['#description'] .= '<br /><br />' . $this->t('Accepts any date in any <a href="https://www.gnu.org/software/tar/manual/html_chapter/tar_7.html#Date-input-formats">GNU Date Input Format</a>. Strings such as today, +2 months, and Dec 9 2004 are all valid.');
 
     // Append token date format to #default_value description.
-    $form['default']['default_value']['#description'] .= '<br /><br />' . $this->t("You may use tokens. Tokens should use the 'html_date' or 'html_datetime' date format. (i.e. @date_format)", ['@date_format' => '[webform-authenticated-user:field_date_of_birth:date:html_date]']);
+    $form['default']['default_value']['#description'] .= '<br /><br />' . $this->t("You may use tokens. Tokens should use the 'html_date' or 'html_datetime' date format. (i.e. @date_format)", ['@date_format' => '[current-user:field_date_of_birth:date:html_date]']);
 
     // Allow custom date formats to be entered.
     $form['display']['format']['#type'] = 'webform_select_other';
-    $form['display']['format']['#other__option_label'] = $this->t('Custom date format...');
+    $form['display']['format']['#other__option_label'] = $this->t('Custom date format…');
     $form['display']['format']['#other__description'] = $this->t('A user-defined date format. See the <a href="http://php.net/manual/function.date.php">PHP manual</a> for available options.');
 
     $form['date'] = [
@@ -186,19 +231,53 @@ public function form(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Date settings'),
     ];
 
-    $form['date']['min'] = [
+    // Date min/max validation.
+    $form['date']['date_date_min'] = [
       '#type' => 'textfield',
-      '#title' => $this->t('Date min'),
+      '#title' => $this->t('Date minimum'),
       '#description' => $this->t('Specifies the minimum date.') . '<br /><br />' . $this->t('Accepts any date in any <a href="https://www.gnu.org/software/tar/manual/html_chapter/tar_7.html#Date-input-formats">GNU Date Input Format</a>. Strings such as today, +2 months, and Dec 9 2004 are all valid.'),
       '#weight' => 10,
     ];
-    $form['date']['max'] = [
+    $form['date']['date_date_max'] = [
       '#type' => 'textfield',
-      '#title' => $this->t('Date max'),
+      '#title' => $this->t('Date maximum'),
       '#description' => $this->t('Specifies the maximum date.') . '<br /><br />' . $this->t('Accepts any date in any <a href="https://www.gnu.org/software/tar/manual/html_chapter/tar_7.html#Date-input-formats">GNU Date Input Format</a>. Strings such as today, +2 months, and Dec 9 2004 are all valid.'),
       '#weight' => 10,
     ];
 
+    // Date/time min/max validation.
+    if ($this->hasProperty('date_date_min')
+      && $this->hasProperty('date_time_min')
+      && $this->hasProperty('date_min')) {
+      $form['validation']['date_min_max_message'] = [
+        '#type' => 'webform_message',
+        '#message_type' => 'warning',
+        '#access' => TRUE,
+        '#message_message' => $this->t("'Date/time' minimum or maximum should not be used with 'Date' or 'Time' specific minimum or maximum.") . '<br/>' .
+          '<strong>' . $this->t('This can cause unexpected validation errors.') . '</strong>',
+        '#message_close' => TRUE,
+        '#message_storage' => WebformMessageElement::STORAGE_SESSION,
+        '#states' => [
+          'visible' => [
+            [':input[name="properties[date_date_min]"]' => ['filled' => TRUE]],
+            [':input[name="properties[date_date_max]"]' => ['filled' => TRUE]],
+            [':input[name="properties[date_time_min]"]' => ['filled' => TRUE]],
+            [':input[name="properties[date_time_max]"]' => ['filled' => TRUE]],
+          ],
+        ],
+      ];
+    }
+    $form['validation']['date_min'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Date/time minimum'),
+      '#description' => $this->t('Specifies the minimum date/time.') . '<br /><br />' . $this->t('Accepts any date in any <a href="https://www.gnu.org/software/tar/manual/html_chapter/tar_7.html#Date-input-formats">GNU Date/Time Input Format</a>. Strings such as today, +2 months, and Dec 9 2004 10:00 PM are all valid.'),
+    ];
+    $form['validation']['date_max'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Date/time maximum'),
+      '#description' => $this->t('Specifies the maximum date/time.') . '<br /><br />' . $this->t('Accepts any date in any <a href="https://www.gnu.org/software/tar/manual/html_chapter/tar_7.html#Date-input-formats">GNU Date/Time Input Format</a>. Strings such as today, +2 months, and Dec 9 2004 10:00 PM are all valid.'),
+    ];
+
     return $form;
   }
 
@@ -213,12 +292,18 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
       $this->setGnuDateInputFormatError($form['properties']['default']['default_value'], $form_state);
     }
 
-    // Validate #min and #max GNU Date Input Format.
-    $input_formats = ['min', 'max'];
-    foreach ($input_formats as $input_format) {
-      if (!$this->validateGnuDateInputFormat($properties, "#$input_format")) {
-        $this->setGnuDateInputFormatError($form['properties']['date'][$input_format], $form_state);
-      }
+    // Validate #*_min and #*_max GNU Date Input Format.
+    if (!$this->validateGnuDateInputFormat($properties, '#date_min')) {
+      $this->setGnuDateInputFormatError($form['properties']['validation']['date_min'], $form_state);
+    }
+    if (!$this->validateGnuDateInputFormat($properties, '#date_max')) {
+      $this->setGnuDateInputFormatError($form['properties']['validation']['date_max'], $form_state);
+    }
+    if (!$this->validateGnuDateInputFormat($properties, '#date_date_min')) {
+      $this->setGnuDateInputFormatError($form['properties']['date']['date_date_min'], $form_state);
+    }
+    if (!$this->validateGnuDateInputFormat($properties, '#date_date_max')) {
+      $this->setGnuDateInputFormatError($form['properties']['date']['date_date_max'], $form_state);
     }
 
     parent::validateConfigurationForm($form, $form_state);
@@ -350,6 +435,11 @@ public static function preValidateDate(&$element, FormStateInterface $form_state
       $input_exists = FALSE;
       $input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
       if (!isset($input['object'])) {
+        // Time picker converts all submitted time values to H:i:s format.
+        // @see \Drupal\webform\Element\WebformTime::validateWebformTime
+        if (isset($element['#date_time_element']) && $element['#date_time_element'] === 'timepicker') {
+          $element['#date_time_format'] = 'H:i:s';
+        }
         $input = $date_class::valueCallback($element, $input, $form_state);
         $form_state->setValueForElement($element, $input);
         $element['#value'] = $input;
@@ -368,16 +458,15 @@ public static function validateDate(&$element, FormStateInterface $form_state, &
     $value = $element['#value'];
     $name = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];
     $date_date_format = (!empty($element['#date_date_format'])) ? $element['#date_date_format'] : DateFormat::load('html_date')->getPattern();
+    $date_time_format = (!empty($element['#date_time_format'])) ? $element['#date_time_format'] : DateFormat::load('html_time')->getPattern();
 
     // Convert DrupalDateTime array and object to ISO datetime.
     if (is_array($value)) {
       $value = ($value['object']) ? $value['object']->format(DateFormat::load('html_datetime')->getPattern()) : '';
     }
     elseif ($value) {
-      // Ensure the input is valid date by creating a date object and comparing
-      // formatted date object to the submitted date value.
-      $datetime = date_create_from_format($date_date_format, $value);
-      if ($datetime === FALSE || date_format($datetime, $date_date_format) != $value) {
+      $datetime = WebformDateHelper::createFromFormat($date_date_format, $value);
+      if ($datetime === FALSE || static::formatDate($date_date_format, $datetime->getTimestamp()) !== $value) {
         $form_state->setError($element, t('%name must be a valid date.', ['%name' => $name]));
         $value = '';
       }
@@ -400,24 +489,46 @@ public static function validateDate(&$element, FormStateInterface $form_state, &
 
     $time = strtotime($value);
 
-    // Ensure that the input is greater than the #min property, if set.
-    if (isset($element['#min'])) {
-      $min = strtotime(date('Y-m-d', strtotime($element['#min'])));
+    // Ensure that the input is greater than the #date_date_min property, if set.
+    if (isset($element['#date_date_min'])) {
+      $min = strtotime(static::formatDate('Y-m-d', strtotime($element['#date_date_min'])));
       if ($time < $min) {
         $form_state->setError($element, t('%name must be on or after %min.', [
           '%name' => $name,
-          '%min' => date($date_date_format, $min),
+          '%min' => static::formatDate($date_date_format, $min),
         ]));
       }
     }
 
-    // Ensure that the input is less than the #max property, if set.
-    if (isset($element['#max'])) {
-      $max = strtotime(date('Y-m-d', strtotime($element['#max'])));
+    // Ensure that the input is less than the #date_date_max property, if set.
+    if (isset($element['#date_date_max'])) {
+      $max = strtotime(static::formatDate('Y-m-d 23:59:59', strtotime($element['#date_date_max'])));
       if ($time > $max) {
         $form_state->setError($element, t('%name must be on or before %max.', [
           '%name' => $name,
-          '%max' => date($date_date_format, $max),
+          '%max' => static::formatDate($date_date_format, $max),
+        ]));
+      }
+    }
+
+    // Ensure that the input is greater than the #date_min property, if set.
+    if (isset($element['#date_min'])) {
+      $min = strtotime($element['#date_min']);
+      if ($time < $min) {
+        $form_state->setError($element, t('%name must be on or after %min.', [
+          '%name' => $name,
+          '%min' => static::formatDate($date_date_format, $min) . ' ' . static::formatDate($date_time_format, $min),
+        ]));
+      }
+    }
+
+    // Ensure that the input is less than the #date_max property, if set.
+    if (isset($element['#date_max'])) {
+      $max = strtotime($element['#date_max']);
+      if ($time > $max) {
+        $form_state->setError($element, t('%name must be on or before %max.', [
+          '%name' => $name,
+          '%max' => static::formatDate($date_date_format, $max) . ' ' . static::formatDate($date_time_format, $max),
         ]));
       }
     }
@@ -432,16 +543,16 @@ public function getTestValues(array $element, WebformInterface $webform, array $
       list($min, $max) = static::datetimeRangeYears($element['#date_year_range']);
     }
     else {
-      $min = !empty($element['#min']) ? strtotime($element['#min']) : strtotime('-10 years');
-      $max = !empty($element['#max']) ? strtotime($element['#max']) : max($min, strtotime('+20 years') ?: PHP_INT_MAX);
+      $min = !empty($element['#date_date_min']) ? strtotime($element['#date_date_min']) : strtotime('-10 years');
+      $max = !empty($element['#date_date_max']) ? strtotime($element['#date_date_max']) : max($min, strtotime('+20 years') ?: PHP_INT_MAX);
     }
-    return date($format, rand($min, $max));
+    return static::formatDate($format, rand($min, $max));
   }
 
   /**
    * Specifies the start and end year to use as a date range.
    *
-   * Copied from: \Drupal\Core\Datetime\Element\DateElementBase::datetimeRangeYears
+   * Copied from: DateElementBase::datetimeRangeYears.
    *
    * @param string $string
    *   A min and max year string like '-3:+1' or '2000:2010' or '2000:+3'.
@@ -451,6 +562,8 @@ public function getTestValues(array $element, WebformInterface $webform, array $
    * @return array
    *   A numerically indexed array, containing the minimum and maximum year
    *   described by this pattern.
+   *
+   * @see \Drupal\Core\Datetime\Element\DateElementBase::datetimeRangeYears
    */
   protected static function datetimeRangeYears($string, $date = NULL) {
     $datetime = new DrupalDateTime();
@@ -492,4 +605,21 @@ protected static function datetimeRangeYears($string, $date = NULL) {
     return [$min_year, $max_year];
   }
 
+  /**
+   * Format custom date.
+   *
+   * @param string $custom_format
+   *   A PHP date format string suitable for input to date().
+   * @param int $timestamp
+   *   (optional) A UNIX timestamp to format.
+   *
+   * @return string
+   *   Formatted date.
+   */
+  protected static function formatDate($custom_format, $timestamp = NULL) {
+    /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
+    $date_formatter = \Drupal::service('date.formatter');
+    return $date_formatter->format($timestamp ?: time(), 'custom', $custom_format);
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/DateList.php b/web/modules/webform/src/Plugin/WebformElement/DateList.php
index a06ca55a954352e6837504aa1380f956b7ce5549..69e0905ab81d0f944c1975bfc4d218d70af4122f 100644
--- a/web/modules/webform/src/Plugin/WebformElement/DateList.php
+++ b/web/modules/webform/src/Plugin/WebformElement/DateList.php
@@ -28,6 +28,8 @@ class DateList extends DateBase {
    */
   public function getDefaultProperties() {
     return [
+      'date_min' => '',
+      'date_max' => '',
       // Date settings.
       'date_part_order' => [
         'year',
@@ -36,12 +38,11 @@ public function getDefaultProperties() {
         'hour',
         'minute',
       ],
-      'date_text_parts' => [
-        'year',
-      ],
+      'date_text_parts' => [],
       'date_year_range' => '1900:2050',
       'date_year_range_reverse' => FALSE,
       'date_increment' => 1,
+      'date_abbreviate' => TRUE,
     ] + parent::getDefaultProperties();
   }
 
@@ -51,7 +52,16 @@ public function getDefaultProperties() {
   public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
     parent::prepare($element, $webform_submission);
 
-    $element['#after_build'][] = [get_class($this), 'afterBuild'];
+    // Remove month abbreviation.
+    // @see \Drupal\Core\Datetime\Element\Datelist::processDatelist
+    if (isset($element['#date_abbreviate']) && $element['#date_abbreviate'] === FALSE) {
+      $element['#date_date_callbacks'][] = '_webform_datelist_date_date_callback';
+    }
+
+    // Remove 'for' from the element's label.
+    $element['#label_attributes']['webform-remove-for-attribute'] = TRUE;
+
+    $element['#attached']['library'][] = 'webform/webform.element.datelist';
   }
 
   /**
@@ -186,6 +196,13 @@ public function form(array $form, FormStateInterface $form_state) {
       '#size' => 4,
       '#weight' => 10,
     ];
+    $form['date']['date_abbreviate'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Abbreviate month'),
+      '#description' => $this->t('If checked, month will be abbreviated to three letters.'),
+      '#return_value' => TRUE,
+    ];
+
     return $form;
   }
 
@@ -214,6 +231,8 @@ protected function setConfigurationFormDefaultValue(array &$form, array &$elemen
    * After build handler for Datelist element.
    */
   public static function afterBuild(array $element, FormStateInterface $form_state) {
+    $element = parent::afterBuild($element, $form_state);
+
     // Reverse years from min:max to max:min.
     // @see \Drupal\Core\Datetime\Element\DateElementBase::datetimeRangeYears
     if (!empty($element['#date_year_range_reverse']) && isset($element['year']) && isset($element['year']['#options'])) {
@@ -258,20 +277,20 @@ public static function validateDatelist(&$element, FormStateInterface $form_stat
     // the $form_state.
     $temp_form_state = clone $form_state;
 
-    // Clear error to ensure that we only capture datelist validation errors.
-    $temp_form_state->clearErrors();
-
     // Validate the date list element.
     DatelistElement::validateDatelist($element, $temp_form_state, $complete_form);
 
     // Copy $temp_form_state errors to $form_state error and alter
     // override default required error message is applicable.
+    $original_errors = $form_state->getErrors();
     $errors = $temp_form_state->getErrors();
     foreach ($errors as $name => $message) {
-      if ($message instanceof TranslatableMarkup && $message->getUntranslatedString() === "The %field date is required.") {
-        $message = $element['#required_error'];
+      if (empty($original_errors[$name])) {
+        if ($message instanceof TranslatableMarkup && $message->getUntranslatedString() === "The %field date is required.") {
+          $message = $element['#required_error'];
+        }
+        $form_state->setErrorByName($name, $message);
       }
-      $form_state->setErrorByName($name, $message);
     }
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/DateTime.php b/web/modules/webform/src/Plugin/WebformElement/DateTime.php
index 1afab5cf329e51ad26bf0ce27594152707e36273..5d733ddbf5bc1b84e04d26fedaaaa5f1d426b64d 100644
--- a/web/modules/webform/src/Plugin/WebformElement/DateTime.php
+++ b/web/modules/webform/src/Plugin/WebformElement/DateTime.php
@@ -41,13 +41,15 @@ public function getDefaultProperties() {
     }
 
     return [
+      'date_min' => '',
+      'date_max' => '',
       // Date settings.
       'date_date_format' => $date_format,
+      'date_date_datepicker_button' => FALSE,
       'date_date_element' => 'date',
       'date_year_range' => '1900:2050',
-      'date_increment' => 1,
+      // Time settings.
       'date_time_format' => $time_format,
-      'date_timezone' => '',
       'date_time_element' => 'time',
       'date_time_min' => '',
       'date_time_max' => '',
@@ -66,6 +68,9 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       $element['#default_value'] = NULL;
     }
 
+    // Remove 'for' from the element's label.
+    $element['#label_attributes']['webform-remove-for-attribute'] = TRUE;
+
     /* Date */
 
     $date_element = (isset($element['#date_date_element'])) ? $element['#date_date_element'] : 'date';
@@ -76,12 +81,23 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       unset($element['date_date_format']);
     }
 
+    // Set date year range.
+    $element += ['#date_year_range' => ''];
+    if (empty($element['#date_year_range'])) {
+      $date_min = $this->getElementProperty($element, 'date_date_min') ?: $this->getElementProperty($element, 'date_min');
+      $min_year = ($date_min) ? static::formatDate('Y', strtotime($date_min)) : '1900';
+      $date_max = $this->getElementProperty($element, 'date_date_max') ?: $this->getElementProperty($element, 'date_max');
+      $max_year = ($date_max) ? static::formatDate('Y', strtotime($date_max)) : '2050';
+      $element['#date_year_range'] = "$min_year:$max_year";
+    }
+
     // Set date format.
     if (!isset($element['#date_date_format'])) {
       $element['#date_date_format'] = $this->getDefaultProperty('date_date_format');
     }
 
-    $element['#date_date_callbacks'][] = '_webform_datetime_datepicker';
+    // Add date callback.
+    $element['#date_date_callbacks'][] = '_webform_datetime_date';
 
     /* Time */
 
@@ -90,13 +106,11 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       $element['#date_time_format'] = $this->getDefaultProperty('date_time_format');
     }
 
-    // Add timepicker callback.
-    $element['#date_time_callbacks'][] = '_webform_datetime_timepicker';
+    // Add time callback.
+    $element['#date_time_callbacks'][] = '_webform_datetime_time';
 
     // Prepare element after date/time formats have been updated.
     parent::prepare($element, $webform_submission);
-
-    $element['#after_build'][] = [get_class($this), 'afterBuildDateTime'];
   }
 
   /**
@@ -150,6 +164,18 @@ public function form(array $form, FormStateInterface $form_state) {
         'none' => $this->t('None - Do not display a date element'),
       ],
     ];
+    $form['date']['date_date_datepicker_button'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Show date picker button'),
+      '#description' => $this->t('If checked, date picker will include a calendar button'),
+      '#return_value' => TRUE,
+      '#states' => [
+        'visible' => [
+          [':input[name="properties[date_date_element]"]' => ['value' => 'datepicker']],
+        ],
+      ],
+
+    ];
     $form['date']['date_date_element_datetime_warning'] = [
       '#type' => 'webform_message',
       '#message_type' => 'warning',
@@ -166,7 +192,7 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['date']['date_date_element_none_warning'] = [
       '#type' => 'webform_message',
       '#message_type' => 'warning',
-      '#message_message' => $this->t('You should consider using a dedicated Time element, instead of this Date/time element, which will preprend the current date to the submitted time.'),
+      '#message_message' => $this->t('You should consider using a dedicated Time element, instead of this Date/time element, which will prepend the current date to the submitted time.'),
       '#access' => TRUE,
       '#states' => [
         'visible' => [
@@ -179,13 +205,13 @@ public function form(array $form, FormStateInterface $form_state) {
       '#type' => 'webform_select_other',
       '#title' => $this->t('Date format'),
       '#options' => [
-        $date_format => $this->t('HTML date - @format (@date)', ['@format' => $date_format, '@date' => date($date_format)]),
-        'l, F j, Y' => $this->t('Long date - @format (@date)', ['@format' => 'l, F j, Y', '@date' => date('l, F j, Y')]),
-        'D, m/d/Y' => $this->t('Medium date - @format (@date)', ['@format' => 'D, m/d/Y', '@date' => date('D, m/d/Y')]),
-        'm/d/Y' => $this->t('Short date - @format (@date)', ['@format' => 'm/d/Y', '@date' => date('m/d/Y')]),
+        $date_format => $this->t('HTML date - @format (@date)', ['@format' => $date_format, '@date' => static::formatDate($date_format)]),
+        'l, F j, Y' => $this->t('Long date - @format (@date)', ['@format' => 'l, F j, Y', '@date' => static::formatDate('l, F j, Y')]),
+        'D, m/d/Y' => $this->t('Medium date - @format (@date)', ['@format' => 'D, m/d/Y', '@date' => static::formatDate('D, m/d/Y')]),
+        'm/d/Y' => $this->t('Short date - @format (@date)', ['@format' => 'm/d/Y', '@date' => static::formatDate('m/d/Y')]),
       ],
-      '#other__option_label' => $this->t('Custom...'),
-      '#other__placeholder' => $this->t('Custom date format...'),
+      '#other__option_label' => $this->t('Custom…'),
+      '#other__placeholder' => $this->t('Custom date format…'),
       '#other__description' => $this->t('Enter date format using <a href="http://php.net/manual/en/function.date.php">Date Input Format</a>.'),
       '#states' => [
         'visible' => [
@@ -195,15 +221,6 @@ public function form(array $form, FormStateInterface $form_state) {
         ],
       ],
     ];
-    $form['date']['date_timezone'] = [
-      '#type' => 'select',
-      '#title' => $this->t('Date timezone override'),
-      '#options' => system_time_zones(TRUE),
-      '#description' => $this->t('Generally this should be left empty and it will be set correctly for the user using the webform.') . ' ' .
-        $this->t('Useful if the default value is empty to designate a desired timezone for dates created in webform processing.') . ' ' .
-        $this->t('If a default date is provided, this value will be ignored, the timezone in the default date takes precedence.') . ' ' .
-        $this->t('Defaults to the value returned by drupal_get_user_timezone().'),
-    ];
     $form['date']['date_year_range'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Date year range'),
@@ -218,23 +235,6 @@ public function form(array $form, FormStateInterface $form_state) {
         ],
       ],
     ];
-    $form['date']['date_increment'] = [
-      '#type' => 'number',
-      '#title' => $this->t('Date increment'),
-      '#description' => $this->t("The increment to use for minutes and seconds, i.e. '15' would show only :00, :15, :30 and :45. Used for HTML5 step values and jQueryUI (fallback) datepicker settings. Defaults to 1 to show every minute."),
-      '#min' => 1,
-      '#attributes' => ['data-webform-states-no-clear' => TRUE],
-      '#states' => [
-        'invisible' => [
-          [':input[name="properties[date_date_element]"]' => ['value' => 'datetime']],
-          'xor',
-          [':input[name="properties[date_date_element]"]' => ['value' => 'datetime-local']],
-          'xor',
-          [':input[name="properties[date_time_element]"]' => ['value' => 'none']],
-        ],
-      ],
-      '#weight' => 10,
-    ];
 
     // Time.
     $form['time'] = [
@@ -263,16 +263,16 @@ public function form(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Time format'),
       '#description' => $this->t("Time format is only applicable for browsers that do not have support for the HTML5 time element. Browsers that support the HTML5 time element will display the time using the user's preferred format."),
       '#options' => [
-        'H:i:s' => $this->t('24 hour with seconds - @format (@time)', ['@format' => 'H:i:s', '@time' => date('H:i:s')]),
-        'H:i' => $this->t('24 hour - @format (@time)', ['@format' => 'H:i', '@time' => date('H:i')]),
-        'g:i:s A' => $this->t('12 hour with seconds - @format (@time)', ['@format' => 'g:i:s A', '@time' => date('g:i:s A')]),
-        'g:i A' => $this->t('12 hour - @format (@time)', ['@format' => 'g:i A', '@time' => date('g:i A')]),
+        'H:i:s' => $this->t('24 hour with seconds - @format (@time)', ['@format' => 'H:i:s', '@time' => static::formatDate('H:i:s')]),
+        'H:i' => $this->t('24 hour - @format (@time)', ['@format' => 'H:i', '@time' => static::formatDate('H:i')]),
+        'g:i:s A' => $this->t('12 hour with seconds - @format (@time)', ['@format' => 'g:i:s A', '@time' => static::formatDate('g:i:s A')]),
+        'g:i A' => $this->t('12 hour - @format (@time)', ['@format' => 'g:i A', '@time' => static::formatDate('g:i A')]),
       ],
-      '#other__option_label' => $this->t('Custom...'),
-      '#other__placeholder' => $this->t('Custom time format...'),
+      '#other__option_label' => $this->t('Custom…'),
+      '#other__placeholder' => $this->t('Custom time format…'),
       '#other__description' => $this->t('Enter time format using <a href="http://php.net/manual/en/function.date.php">Time Input Format</a>.'),
       '#states' => [
-        'invisible'  => [
+        'invisible' => [
           [':input[name="properties[date_date_element]"]' => ['value' => 'datetime']],
           'or',
           [':input[name="properties[date_date_element]"]' => ['value' => 'datetime-local']],
@@ -286,20 +286,20 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['time']['date_time_container'] = $this->getFormInlineContainer();
     $form['time']['date_time_container']['date_time_min'] = [
       '#type' => 'webform_time',
-      '#title' => $this->t('Time min'),
+      '#title' => $this->t('Time minimum'),
       '#description' => $this->t('Specifies the minimum time.'),
       '#states' => [
-        'invisible'  => [
+        'invisible' => [
           [':input[name="properties[date_time_element]"]' => ['value' => 'none']],
         ],
       ],
     ];
     $form['time']['date_time_container']['date_time_max'] = [
       '#type' => 'webform_time',
-      '#title' => $this->t('Time max'),
+      '#title' => $this->t('Time maximum'),
       '#description' => $this->t('Specifies the maximum time.'),
       '#states' => [
-        'invisible'  => [
+        'invisible' => [
           [':input[name="properties[date_time_element]"]' => ['value' => 'none']],
         ],
       ],
@@ -309,8 +309,8 @@ public function form(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Time step'),
       '#description' => $this->t('Specifies the minute intervals.'),
       '#options' => [
-        '' => $this->t('1 minute'),
-        30 => $this->t('5 minutes'),
+        60 => $this->t('1 minute'),
+        300 => $this->t('5 minutes'),
         600 => $this->t('10 minutes'),
         900 => $this->t('15 minutes'),
         1200 => $this->t('20 minutes'),
@@ -318,6 +318,11 @@ public function form(array $form, FormStateInterface $form_state) {
       ],
       '#other__type' => 'number',
       '#other__description' => $this->t('Enter interval in seconds.'),
+      '#states' => [
+        'invisible' => [
+          [':input[name="properties[date_time_element]"]' => ['value' => 'none']],
+        ],
+      ],
     ];
     return $form;
   }
@@ -377,25 +382,4 @@ public function getConfigurationFormProperties(array &$form, FormStateInterface
     return $properties;
   }
 
-  /**
-   * After build handler for Datetime elements.
-   */
-  public static function afterBuildDateTime(array $element, FormStateInterface $form_state) {
-    if (isset($element['time'])) {
-      if (!empty($element['#date_time_min'])) {
-        $element['time']['#min'] = $element['#date_time_min'];
-        $element['time']['#attributes']['min'] = $element['#date_time_min'];
-      }
-      if (!empty($element['#date_time_max'])) {
-        $element['time']['#max'] = $element['#date_time_max'];
-        $element['time']['#attributes']['max'] = $element['#date_time_max'];
-      }
-      if (!empty($element['#date_time_step'])) {
-        $element['time']['#max'] = $element['#date_time_step'];
-        $element['time']['#attributes']['step'] = $element['#date_time_step'];
-      }
-    }
-    return $element;
-  }
-
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/Details.php b/web/modules/webform/src/Plugin/WebformElement/Details.php
index b039ddfd5ae104510671a05b9de467352ddefacb..0063e7d3e018beed6b77d35e4cd3721cb18ba02d 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Details.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Details.php
@@ -21,10 +21,27 @@ class Details extends ContainerBase {
    * {@inheritdoc}
    */
   public function getDefaultProperties() {
-    return [
+    $properties = [
+      // Description/Help.
       'help' => '',
+      'help_title' => '',
+      'description' => '',
+      'more' => '',
+      'more_title' => '',
+      // Title.
+      'title_display' => '',
+      // Details.
       'open' => FALSE,
     ] + parent::getDefaultProperties();
+
+    // Issue #2971848: [8.6.x] Details elements allow specifying attributes
+    // for the <summary> element.
+    // @todo Remove the below if/then when only 8.6.x is supported.
+    if (version_compare(\Drupal::VERSION, '8.6', '>=')) {
+      $properties['summary_attributes'] = [];
+    }
+
+    return $properties;
   }
 
   /**
diff --git a/web/modules/webform/src/Plugin/WebformElement/Email.php b/web/modules/webform/src/Plugin/WebformElement/Email.php
index 6947fec84e16ec8259641ff11cbf74553434f4dd..b7a3444b477d09394ff732bd3d4fe07516c4d045 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Email.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Email.php
@@ -21,7 +21,10 @@ class Email extends TextBase {
    * {@inheritdoc}
    */
   public function getDefaultProperties() {
-    return parent::getDefaultProperties() + $this->getDefaultMultipleProperties();
+    return [
+      'input_hide' => FALSE,
+    ] + parent::getDefaultProperties()
+      + $this->getDefaultMultipleProperties();
   }
 
   /**
diff --git a/web/modules/webform/src/Plugin/WebformElement/EntityAutocomplete.php b/web/modules/webform/src/Plugin/WebformElement/EntityAutocomplete.php
index 02896d7494a0588f7cf488e9694208a775a4d8e7..3aa4d18f3e571f701e39d0305e3352878de0358e 100644
--- a/web/modules/webform/src/Plugin/WebformElement/EntityAutocomplete.php
+++ b/web/modules/webform/src/Plugin/WebformElement/EntityAutocomplete.php
@@ -90,7 +90,6 @@ public function hasMultipleValues(array $element) {
    */
   public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
     parent::prepare($element, $webform_submission);
-    $element['#after_build'][] = [get_class($this), 'afterBuildEntityAutocomplete'];
 
     // Remove maxlength.
     $element['#maxlength'] = NULL;
@@ -105,19 +104,21 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
   }
 
   /**
-   * Form API callback. After build set the #element_validate handler.
+   * {@inheritdoc}
    */
-  public static function afterBuildEntityAutocomplete(array $element, FormStateInterface $form_state) {
+  protected function prepareElementValidateCallbacks(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    parent::prepareElementValidateCallbacks($element, $webform_submission);
     $element['#element_validate'][] = ['\Drupal\webform\Plugin\WebformElement\EntityAutocomplete', 'validateEntityAutocomplete'];
-    return $element;
   }
 
   /**
    * Form API callback. Remove target id property and create an array of entity ids.
    */
   public static function validateEntityAutocomplete(array &$element, FormStateInterface $form_state) {
-    $name = $element['#name'];
-    $value = $form_state->getValue($name);
+    // Must use ::getValue($element['#parents']) because $element['#value'] is
+    // not being updated.
+    // @see \Drupal\Core\Entity\Element\EntityAutocomplete::validateEntityAutocomplete
+    $value = $form_state->getValue($element['#parents']);
     if (empty($value) || !is_array($value)) {
       return;
     }
diff --git a/web/modules/webform/src/Plugin/WebformElement/Fieldset.php b/web/modules/webform/src/Plugin/WebformElement/Fieldset.php
index ef09df5ec4d3c4bca781d0555af5de97dc1f0e51..4f2c610ab263d07351e80b0abe964115d29756ce 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Fieldset.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Fieldset.php
@@ -20,8 +20,15 @@ class Fieldset extends ContainerBase {
    */
   public function getDefaultProperties() {
     return [
+      // Description/Help.
       'help' => '',
+      'help_title' => '',
+      'description' => '',
+      'more' => '',
+      'more_title' => '',
+      // Title.
       'title_display' => '',
+      'description_display' => '',
     ] + parent::getDefaultProperties();
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/Hidden.php b/web/modules/webform/src/Plugin/WebformElement/Hidden.php
index b243be176471c6bdb18bb3e58ca61f9006cea0c4..171bbc0b8683ea1c2b61f4c5a1a51d7ac2c6dcaa 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Hidden.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Hidden.php
@@ -2,6 +2,10 @@
 
 namespace Drupal\webform\Plugin\WebformElement;
 
+use Drupal\webform\WebformInterface;
+
+use Drupal\Core\Form\FormStateInterface;
+
 /**
  * Provides a 'hidden' element.
  *
@@ -22,7 +26,7 @@ public function getDefaultProperties() {
     return [
       // Element settings.
       'title' => '',
-      'value' => '',
+      'default_value' => '',
       'prepopulate' => FALSE,
     ];
   }
@@ -34,4 +38,32 @@ public function preview() {
     return [];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getTestValues(array $element, WebformInterface $webform, array $options = []) {
+    // Hidden elements should never get a test value.
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+
+    // Remove the default section under the advanced tab.
+    unset($form['default']);
+
+    // Add the default value textarea to the element's main settings.
+    $form['element']['default_value'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Default value'),
+      '#description' => $this->t('The default value of the webform element.'),
+      '#maxlength' => NULL,
+    ];
+
+    return $form;
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/Item.php b/web/modules/webform/src/Plugin/WebformElement/Item.php
index c17cf7179d6a466601dea72f756b57a7e1fc067d..f65f12977acb85998b3b336e48e71ce4268ea534 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Item.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Item.php
@@ -26,6 +26,7 @@ public function getDefaultProperties() {
       'title' => '',
       // Description/Help.
       'help' => '',
+      'help_title' => '',
       'description' => '',
       'more' => '',
       'more_title' => '',
@@ -42,8 +43,8 @@ public function getDefaultProperties() {
   /**
    * {@inheritdoc}
    */
-  public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
-    parent::prepare($element, $webform_submission);
+  protected function prepareElementValidateCallbacks(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    parent::prepareElementValidateCallbacks($element, $webform_submission);
     $element['#element_validate'][] = [get_class($this), 'validateItem'];
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/NumericBase.php b/web/modules/webform/src/Plugin/WebformElement/NumericBase.php
index 01ccc554e269866ccb47da81cc0024a4c0461601..96d5bae3995605f6c705fc1d454264840eb0249d 100644
--- a/web/modules/webform/src/Plugin/WebformElement/NumericBase.php
+++ b/web/modules/webform/src/Plugin/WebformElement/NumericBase.php
@@ -61,14 +61,14 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['number']['number_container'] = $this->getFormInlineContainer();
     $form['number']['number_container']['min'] = [
       '#type' => 'number',
-      '#title' => $this->t('Min'),
+      '#title' => $this->t('Minimum'),
       '#description' => $this->t('Specifies the minimum value.'),
       '#step' => 'any',
       '#size' => 4,
     ];
     $form['number']['number_container']['max'] = [
       '#type' => 'number',
-      '#title' => $this->t('Max'),
+      '#title' => $this->t('Maximum'),
       '#description' => $this->t('Specifies the maximum value.'),
       '#step' => 'any',
       '#size' => 4,
diff --git a/web/modules/webform/src/Plugin/WebformElement/OptionsBase.php b/web/modules/webform/src/Plugin/WebformElement/OptionsBase.php
index d0169efbbab0abf7d7cded111ef2fdf38df10dc2..76f8055581cd701ba9ca9cbc0bf749efa0bbc3ba 100644
--- a/web/modules/webform/src/Plugin/WebformElement/OptionsBase.php
+++ b/web/modules/webform/src/Plugin/WebformElement/OptionsBase.php
@@ -2,11 +2,9 @@
 
 namespace Drupal\webform\Plugin\WebformElement;
 
-use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Form\OptGroup;
 use Drupal\Core\Render\Markup;
-use Drupal\webform\Utility\WebformArrayHelper;
 use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\Utility\WebformOptionsHelper;
 use Drupal\webform\Plugin\WebformElementBase;
@@ -19,6 +17,8 @@
  */
 abstract class OptionsBase extends WebformElementBase {
 
+  use TextBaseTrait;
+
   /**
    * Export delta for multiple options.
    *
@@ -39,9 +39,8 @@ abstract class OptionsBase extends WebformElementBase {
   public function getDefaultProperties() {
     $properties = parent::getDefaultProperties();
 
-    // Issue #2836374: Wrapper attributes are not supported by composite
-    // elements, this includes radios, checkboxes, and buttons.
-    if (preg_match('/(radios|checkboxes|buttons|tableselect|tableselect_sort|table_sort)$/', $this->getPluginId())) {
+    // Wrapper attributes are not supported by table elements.
+    if (preg_match('/(tableselect|tableselect_sort|table_sort)$/', $this->getPluginId())) {
       unset($properties['wrapper_attributes']);
     }
 
@@ -61,10 +60,10 @@ public function getDefaultProperties() {
     // Add other properties to elements that include the other text field.
     if ($this->isOptionsOther()) {
       $properties += [
-        'other__option_label' => $this->t('Other...'),
+        'other__option_label' => $this->t('Other…'),
         'other__type' => 'textfield',
         'other__title' => '',
-        'other__placeholder' => $this->t('Enter other...'),
+        'other__placeholder' => $this->t('Enter other…'),
         'other__description' => '',
         // Text field or textarea.
         'other__size' => '',
@@ -77,6 +76,14 @@ public function getDefaultProperties() {
         'other__min' => '',
         'other__max' => '',
         'other__step' => '',
+        // Counter.
+        'other__counter_type' => '',
+        'other__counter_minimum' => '',
+        'other__counter_minimum_message' => '',
+        'other__counter_maximum' => '',
+        'other__counter_maximum_message' => '',
+        // Wrapper.
+        'wrapper_type' => 'fieldset',
       ];
     }
 
@@ -187,7 +194,7 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
 
     // Randomize options.
     if (isset($element['#options']) && !empty($element['#options_randomize'])) {
-      $element['#options'] = WebformArrayHelper::shuffle($element['#options']);
+      $element['#options'] = WebformElementHelper::randomize($element['#options']);
     }
 
     // Options description display must be set to trigger the description display.
@@ -215,7 +222,7 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
             $element['#options'][$default_value] = $default_value;
           }
         }
-    }
+      }
     }
 
     // If the element is #required and the #default_value is an empty string
@@ -226,6 +233,16 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareElementValidateCallbacks(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    if ($this->hasMultipleValues($element)) {
+      $element['#element_validate'][] = [get_class($this), 'validateMultipleOptions'];
+    }
+    parent::prepareElementValidateCallbacks($element, $webform_submission);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -491,14 +508,13 @@ public function buildExportRecord(array $element, WebformSubmissionInterface $we
    * Form API callback. Remove unchecked options from value array.
    */
   public static function validateMultipleOptions(array &$element, FormStateInterface $form_state, array &$completed_form) {
-    $name = $element['#name'];
-    $values = $form_state->getValue($name) ?: [];
+    $values = $element['#value'] ?: [];
     // Filter unchecked/unselected options whose value is 0.
     $values = array_filter($values, function ($value) {
       return $value !== 0;
     });
     $values = array_values($values);
-    $form_state->setValue($name, $values);
+    $form_state->setValueForElement($element, $values);
   }
 
   /**
@@ -548,6 +564,28 @@ public function getElementSelectorOptions(array $element) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorSourceValues(array $element) {
+    if ($this->hasMultipleValues($element) && $this->hasMultipleWrapper()) {
+      return [];
+    }
+
+    $plugin_id = $this->getPluginId();
+    $name = $element['#webform_key'];
+    $options = OptGroup::flattenOptions($element['#options']);
+    if ($this->getElementSelectorInputsOptions($element)) {
+      $other_type = $this->getOptionsOtherType();
+      $multiple = ($this->hasMultipleValues($element) && $other_type === 'select') ? '[]' : '';
+      return [":input[name=\"{$name}[$other_type]$multiple\"]" => $options];
+    }
+    else {
+      $multiple = ($this->hasMultipleValues($element) && strpos($plugin_id, 'select') !== FALSE) ? '[]' : '';
+      return [":input[name=\"$name$multiple\"]" => $options];
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -556,24 +594,36 @@ public function getElementSelectorInputValue($selector, $trigger, array $element
       $input_name = WebformSubmissionConditionsValidator::getSelectorInputName($selector);
       $other_type = WebformSubmissionConditionsValidator::getInputNameAsArray($input_name, 1);
       $value = $this->getRawValue($element, $webform_submission);
+
+      // Handle edge case where the other element's value has
+      // not been processed.
+      // @see https://www.drupal.org/project/webform/issues/3000202
+      /** @var \Drupal\webform\Element\WebformOtherBase $class */
+      $class = $this->getFormElementClassDefinition();
+      $type = $class::getElementType();
+      if (is_array($value) && count($value) === 2 && isset($value[$type]) && isset($value['other'])) {
+        $value = $class::processValue($element, $value);
+      }
+
+      $options = OptGroup::flattenOptions($element['#options']);
       if ($other_type === 'other') {
         if ($this->hasMultipleValues($element)) {
-          $other_value = array_diff($value, array_keys($element['#options']));
+          $other_value = array_diff($value, array_keys($options));
           return ($other_value) ? implode(', ', $other_value) : NULL;
         }
         else {
           // Make sure other value is not valid option.
-          return ($value && !isset($element['#options'][$value])) ? $value : NULL;
+          return ($value && !isset($options[$value])) ? $value : NULL;
         }
       }
       else {
         if ($this->hasMultipleValues($element)) {
           // Return array of valid #options.
-          return array_intersect($value, array_keys($element['#options']));
+          return array_intersect($value, array_keys($options));
         }
         else {
           // Return valid #option.
-          return (isset($element['#options'][$value])) ? $value : NULL;
+          return (isset($options[$value])) ? $value : NULL;
         }
       }
     }
@@ -588,13 +638,13 @@ public function getElementSelectorInputValue($selector, $trigger, array $element
   public function form(array $form, FormStateInterface $form_state) {
     $form = parent::form($form, $form_state);
 
-    $form['default']['default_value']['#description'] = $this->t('The default value of the field identified by its key.');
+    $form['default']['default_value']['#description'] .= ' ' . $this->t('The default value of the field identified by its key.');
 
     // Issue #2836374: Wrapper attributes are not supported by composite
     // elements, this includes radios, checkboxes, and buttons.
     if (preg_match('/(radios|checkboxes|buttons)/', $this->getPluginId())) {
       $t_args = [
-        '@name' => Unicode::strtolower($this->getPluginLabel()),
+        '@name' => mb_strtolower($this->getPluginLabel()),
         ':href' => 'https://www.drupal.org/node/2836364',
       ];
       $form['element_attributes']['#description'] = $this->t('Please note: That the below custom element attributes will also be applied to the @name fieldset wrapper. (<a href=":href">Issue #2836374</a>)', $t_args);
@@ -645,9 +695,9 @@ public function form(array $form, FormStateInterface $form_state) {
     $default_empty_option = $this->configFactory->get('webform.settings')->get('element.default_empty_option');
     if ($default_empty_option) {
       $default_empty_option_required = $this->configFactory->get('webform.settings')->get('element.default_empty_option_required') ?: $this->t('- Select -');
-      $form['options']['empty_option']['#description'] .= '<br />' . $this->t('Required elements default to: %required', ['%required' => $default_empty_option_required]);
+      $form['options']['empty_option']['#description'] .= '<br />' . $this->t('Required elements defaults to: %required', ['%required' => $default_empty_option_required]);
       $default_empty_option_optional = $this->configFactory->get('webform.settings')->get('element.default_empty_option_optional') ?: $this->t('- None -');
-      $form['options']['empty_option']['#description'] .= '<br />' . $this->t('Optional elements default to: %optional', ['%optional' => $default_empty_option_optional]);
+      $form['options']['empty_option']['#description'] .= '<br />' . $this->t('Optional elements defaults to: %optional', ['%optional' => $default_empty_option_optional]);
     }
     $form['options']['empty_value'] = [
       '#type' => 'textfield',
@@ -676,6 +726,13 @@ public function form(array $form, FormStateInterface $form_state) {
         [':input[name="properties[other__type]"]' => ['value' => 'number']],
       ],
     ];
+    $states_textbase = [
+      'visible' => [
+        [':input[name="properties[other__type]"]' => ['value' => 'textfield']],
+        'or',
+        [':input[name="properties[other__type]"]' => ['value' => 'textarea']],
+      ],
+    ];
     $states_textarea = [
       'visible' => [
         ':input[name="properties[other__type]"]' => ['value' => 'textarea'],
@@ -760,7 +817,7 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['options_other']['other__number_container'] = $this->getFormInlineContainer();
     $form['options_other']['other__number_container']['other__min'] = [
       '#type' => 'number',
-      '#title' => $this->t('Other min'),
+      '#title' => $this->t('Other minimum'),
       '#description' => $this->t('Specifies the minimum value.'),
       '#step' => 'any',
       '#size' => 4,
@@ -768,7 +825,7 @@ public function form(array $form, FormStateInterface $form_state) {
     ];
     $form['options_other']['other__number_container']['other__max'] = [
       '#type' => 'number',
-      '#title' => $this->t('Other max'),
+      '#title' => $this->t('Other maximum'),
       '#description' => $this->t('Specifies the maximum value.'),
       '#step' => 'any',
       '#size' => 4,
@@ -783,6 +840,11 @@ public function form(array $form, FormStateInterface $form_state) {
       '#states' => $states_number,
     ];
 
+    $form['options_other']['other__textbase_container'] = [
+      '#type' => 'container',
+      '#states' => $states_textbase,
+    ] + $this->buildCounterForm('other__', 'Other count');
+
     // Add hide/show #format_items based on #multiple.
     if ($this->supportsMultipleValues() && $this->hasProperty('multiple')) {
       $form['display']['format_items']['#states'] = [
diff --git a/web/modules/webform/src/Plugin/WebformElement/PasswordConfirm.php b/web/modules/webform/src/Plugin/WebformElement/PasswordConfirm.php
index 5e7d91302b8b7bef9a8b5c8a32c620a618bfa1c4..60d3ab2ef637813ef129ed40541f1553a5dbecba 100644
--- a/web/modules/webform/src/Plugin/WebformElement/PasswordConfirm.php
+++ b/web/modules/webform/src/Plugin/WebformElement/PasswordConfirm.php
@@ -19,14 +19,38 @@
  */
 class PasswordConfirm extends Password {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultProperties() {
+    return [
+      'wrapper_type' => 'fieldset',
+    ] + parent::getDefaultProperties();
+  }
+
   /**
    * {@inheritdoc}
    */
   public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
     parent::prepare($element, $webform_submission);
+    $element['#theme_wrappers'] = [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareElementValidateCallbacks(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    parent::prepareElementValidateCallbacks($element, $webform_submission);
     $element['#element_validate'][] = [get_class($this), 'validatePasswordConfirm'];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareElementPreRenderCallbacks(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    $element['#pre_render'] = [[get_called_class(), 'preRenderWebformCompositeFormElement']];
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -60,9 +84,23 @@ public function setDefaultValue(array &$element) {
    * Form API callback. Convert password confirm array to single value.
    */
   public static function validatePasswordConfirm(array &$element, FormStateInterface $form_state, array &$completed_form) {
-    $name = $element['#name'];
-    $value = $form_state->getValue($name);
-    $form_state->setValue($name, $value['pass1']);
+    $value = $element['#value'];
+    $form_state->setValueForElement($element, $value['pass1']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+
+    // Remove unsupported title and description display from composite elements.
+    if ($this->isComposite()) {
+      unset($form['form']['display_container']['title_display']['#options']['inline']);
+      unset($form['form']['display_container']['description_display']['#options']['tooltip']);
+    }
+
+    return $form;
   }
 
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/ProcessedText.php b/web/modules/webform/src/Plugin/WebformElement/ProcessedText.php
index deb1ef30e32e7be544798f675bc3e8f0fa02296c..4ce01334e9937c6e76b89e094492ca3cbe428b24 100644
--- a/web/modules/webform/src/Plugin/WebformElement/ProcessedText.php
+++ b/web/modules/webform/src/Plugin/WebformElement/ProcessedText.php
@@ -38,6 +38,7 @@ public function getDefaultProperties() {
 
     return [
       'wrapper_attributes' => [],
+      'label_attributes' => [],
       // Markup settings.
       'text' => '',
       'format' => $default_format ,
@@ -83,7 +84,7 @@ public function form(array $form, FormStateInterface $form_state) {
     // which closes the original modal.
     // @todo Remove the below workaround once this issue is resolved.
     if (!$form_state->getUserInput() && \Drupal::currentUser()->hasPermission('administer webform')) {
-      drupal_set_message($this->t('Processed text element can not be opened within a modal. Please see <a href="https://www.drupal.org/node/2741877">Issue #2741877: Nested modals don\'t work</a>.'), 'warning');
+      $this->messenger()->addWarning($this->t('Processed text element can not be opened within a modal. Please see <a href="https://www.drupal.org/node/2741877">Issue #2741877: Nested modals don\'t work</a>.'));
     }
     $form = parent::form($form, $form_state);
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/Radios.php b/web/modules/webform/src/Plugin/WebformElement/Radios.php
index a44a5766f633d04c0909a31c37a7f3ac7c290c2e..f7e42142df0496f468c85a3f28905295799aa062 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Radios.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Radios.php
@@ -27,6 +27,8 @@ public function getDefaultProperties() {
       'options_description_display' => 'description',
       // iCheck settings.
       'icheck' => '',
+      // Wrapper.
+      'wrapper_type' => 'fieldset',
     ] + parent::getDefaultProperties();
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/Range.php b/web/modules/webform/src/Plugin/WebformElement/Range.php
index 821802a038efd491799e235b00ef1012b32e3f5d..8ee29bb576c1d64643cd8e80395a68aaa496e6fd 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Range.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Range.php
@@ -57,8 +57,6 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       '#max' => $this->getDefaultProperty('max'),
     ];
 
-    $element['#element_validate'][] = [get_class($this), 'validateRange'];
-
     // If no custom range output is defined then exit.
     if (empty($element['#output'])) {
       return;
@@ -83,6 +81,9 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       // Create output (number) element.
       $output = [
         '#type' => 'number',
+        '#title' => $element['#title'],
+        '#title_display' => 'invisible',
+        '#id' => $webform_key . '__output',
         '#name' => $webform_key . '__output',
       ];
 
@@ -90,7 +91,7 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       $properties = ['#min', '#max', '#step', '#disabled'];
       $output += array_intersect_key($element, array_combine($properties, $properties));
 
-      // Copy custom output properties to ouput element.
+      // Copy custom output properties to output element.
       foreach ($element as $key => $value) {
         if (strpos($key, '#output__') === 0) {
           $output_key = str_replace('#output__', '#', $key);
@@ -149,6 +150,14 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
     $element['#attached']['library'][] = 'webform/webform.element.range';
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareElementValidateCallbacks(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    parent::prepareElementValidateCallbacks($element, $webform_submission);
+    $element['#element_validate'][] = [get_class($this), 'validateRange'];
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -223,10 +232,9 @@ public function form(array $form, FormStateInterface $form_state) {
    * @see \Drupal\Core\Render\Element\Range::valueCallback
    */
   public static function validateRange(array &$element, FormStateInterface $form_state, array &$completed_form) {
-    $name = $element['#name'];
-    $value = $form_state->getValue($name);
+    $value = $element['#value'];
     $value = ($value === 0) ? '0' : (string) $value;
-    $form_state->setValue($name, $value);
+    $form_state->setValueForElement($element, $value);
   }
 
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/Select.php b/web/modules/webform/src/Plugin/WebformElement/Select.php
index 6ebf30b47d70a8da2442623cebd67f2655d0cddf..b66c44f1b891f535cafce404506fefeee6e05d4e 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Select.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Select.php
@@ -3,7 +3,6 @@
 namespace Drupal\webform\Plugin\WebformElement;
 
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
@@ -31,6 +30,7 @@ public function getDefaultProperties() {
       'empty_value' => '',
       'select2' => FALSE,
       'chosen' => FALSE,
+      'placeholder' => '',
     ] + parent::getDefaultProperties();
   }
 
@@ -64,13 +64,36 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       }
     }
 
-    if (!empty($element['#multiple'])) {
-      $element['#element_validate'][] = [get_class($this), 'validateMultipleOptions'];
+    // If select2 or chosen is not available, see if we can use the alternative.
+    if (isset($element['#select2'])
+      && !$this->librariesManager->isIncluded('jquery.select2')
+      && $this->librariesManager->isIncluded('jquery.chosen')) {
+      $element['#chosen'] = TRUE;
+    }
+    elseif (isset($element['#chosen'])
+      && !$this->librariesManager->isIncluded('jquery.chosen')
+      && $this->librariesManager->isIncluded('jquery.select2')) {
+      $element['#select2'] = TRUE;
     }
 
-    parent::prepare($element, $webform_submission);
+    // Enhance select element using select2 or chosen.
+    if (isset($element['#select2']) && $this->librariesManager->isIncluded('jquery.select2')) {
+      $element['#attached']['library'][] = 'webform/webform.element.select2';
+      $element['#attributes']['class'][] = 'js-webform-select2';
+      $element['#attributes']['class'][] = 'webform-select2';
+    }
+    elseif (isset($element['#chosen']) && $this->librariesManager->isIncluded('jquery.chosen')) {
+      $element['#attached']['library'][] = 'webform/webform.element.chosen';
+      $element['#attributes']['class'][] = 'js-webform-chosen';
+      $element['#attributes']['class'][] = 'webform-chosen';
+    }
 
-    WebformElementHelper::enhanceSelect($element);
+    // Set placeholder as data attributes for select2 or chosen elements.
+    if (!empty($element['#placeholder'])) {
+      $element['#attributes']['data-placeholder'] = $element['#placeholder'];
+    }
+
+    parent::prepare($element, $webform_submission);
   }
 
   /**
@@ -78,6 +101,9 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
    */
   public function form(array $form, FormStateInterface $form_state) {
     $form = parent::form($form, $form_state);
+
+    // Select2 and/or Chosen enhancements.
+    // @see \Drupal\webform\Plugin\WebformElement\WebformCompositeBase::form
     $form['options']['select2'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Select2'),
@@ -102,7 +128,6 @@ public function form(array $form, FormStateInterface $form_state) {
           ':input[name="properties[select2]"]' => ['checked' => TRUE],
         ],
       ],
-
     ];
     if ($this->librariesManager->isExcluded('jquery.chosen')) {
       $form['options']['chosen']['#access'] = FALSE;
@@ -111,10 +136,36 @@ public function form(array $form, FormStateInterface $form_state) {
       $form['options']['select_message'] = [
         '#type' => 'webform_message',
         '#message_type' => 'warning',
-        '#message_message' => $this->t('Select2 and Chosen provide very similar functionality, only one can enabled.'),
+        '#message_message' => $this->t('Select2 and Chosen provide very similar functionality, only one should be enabled.'),
         '#access' => TRUE,
       ];
     }
+
+    // Add states to placeholder if custom library is supported and the
+    // select menu supports multiple values.
+    $placeholder_states = [];
+    if (!$this->librariesManager->isExcluded('jquery.select2')) {
+      $placeholder_states[] = [':input[name="properties[select2]"]' => ['checked' => TRUE]];
+    }
+    if (!$this->librariesManager->isExcluded('jquery.chosen')) {
+      if (isset($form['form']['placeholder']['#states']['visible'])) {
+        $placeholder_states[] = 'or';
+      }
+      $placeholder_states[] = [':input[name="properties[chosen]"]' => ['checked' => TRUE]];
+    }
+    if ($placeholder_states) {
+      $form['form']['placeholder']['#states']['visible'] = [
+        [
+        ':input[name="properties[multiple][container][cardinality]"]' => ['value' => 'number'],
+        ':input[name="properties[multiple][container][cardinality_number]"]' => ['!value' => 1],
+        ],
+        $placeholder_states,
+      ];
+    }
+    else {
+      $form['form']['placeholder']['#access'] = FALSE;
+    }
+
     return $form;
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/Table.php b/web/modules/webform/src/Plugin/WebformElement/Table.php
index bc5e6d130890875ce46f0bde5cfcd6f8f6269799..2cb04f008e5cd81db23b7d8dcea8c059e1f1765b 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Table.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Table.php
@@ -6,7 +6,6 @@
 use Drupal\Core\Render\Element;
 use Drupal\webform\Plugin\WebformElementBase;
 use Drupal\webform\WebformInterface;
-use Drupal\Component\Utility\Unicode;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
@@ -158,7 +157,7 @@ protected function formatTextItem(array $element, WebformSubmissionInterface $we
     // Add divider between (optional) header.
     if (!empty($element['#header'])) {
       $lines = explode(PHP_EOL, trim($html));
-      $lines[0] .= PHP_EOL . str_repeat('-', Unicode::strlen($lines[0]));
+      $lines[0] .= PHP_EOL . str_repeat('-', mb_strlen($lines[0]));
       $html = implode(PHP_EOL, $lines);
     }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/Telephone.php b/web/modules/webform/src/Plugin/WebformElement/Telephone.php
index a1ee17ba17bea1b5c9ab0b79ef66c43086cb736a..2c02ad15aa3356473c41b39216992a2144e06c50 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Telephone.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Telephone.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\webform\Plugin\WebformElement;
 
+use Drupal\webform\Element\WebformMessage as WebformMessageElement;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Locale\CountryManager;
 use Drupal\webform\WebformInterface;
@@ -12,10 +13,11 @@
  *
  * @WebformElement(
  *   id = "tel",
- *   api = "https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Render!Element!Tel.php/class/Tel",
- *   label = @Translation("Telephone"),
- *   description = @Translation("Provides a form element for entering a telephone number."),
- *   category = @Translation("Advanced elements"),
+ *   api =
+ *   "https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Render!Element!Tel.php/class/Tel",
+ *   label = @Translation("Telephone"), description = @Translation("Provides a
+ *   form element for entering a telephone number."), category =
+ *   @Translation("Advanced elements"),
  * )
  */
 class Telephone extends TextBase {
@@ -24,11 +26,23 @@ class Telephone extends TextBase {
    * {@inheritdoc}
    */
   public function getDefaultProperties() {
-    return [
-      'multiple' => FALSE,
-      'international' => FALSE,
-      'international_initial_country' => '',
-    ] + parent::getDefaultProperties();
+    $properties = [
+        'input_hide' => FALSE,
+        'multiple' => FALSE,
+        'international' => FALSE,
+        'international_initial_country' => '',
+      ] + parent::getDefaultProperties();
+
+    // Add support for telephone_validation.module.
+    if (\Drupal::moduleHandler()->moduleExists('telephone_validation')) {
+      $properties += [
+        'telephone_validation_format' => '',
+        'telephone_validation_country' => '',
+        'telephone_validation_countries' => [],
+      ];
+    }
+
+    return $properties;
   }
 
   /**
@@ -45,6 +59,41 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       if (!empty($element['#international_initial_country'])) {
         $element['#attributes']['data-webform-telephone-international-initial-country'] = $element['#international_initial_country'];
       }
+
+      // The utilsScript is fetched when the page has finished loading to
+      // prevent blocking.
+      // @see https://github.com/jackocnr/intl-tel-input
+      $utils_script = '/libraries/jquery.intl-tel-input/build/js/utils.js';
+      // Load utils.js from CDN defined in webform.libraries.yml.
+      if (!file_exists(DRUPAL_ROOT . $utils_script)) {
+        /** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */
+        $library_discovery = \Drupal::service('library.discovery');
+        $intl_tel_input_library = $library_discovery->getLibraryByName('webform', 'libraries.jquery.intl-tel-input');
+        $cdn = reset($intl_tel_input_library['cdn']);
+        $utils_script = $cdn . 'build/js/utils.js';
+      }
+      $element['#attached']['drupalSettings']['webform']['intlTelInput']['utilsScript'] = $utils_script;
+    }
+
+    // Add support for telephone_validation.module.
+    if (\Drupal::moduleHandler()->moduleExists('telephone_validation')) {
+      $format = $this->getElementProperty($element, 'telephone_validation_format');
+      if ($format === \libphonenumber\PhoneNumberFormat::NATIONAL) {
+        $country = (array) $this->getElementProperty($element, 'telephone_validation_country');
+      }
+      else {
+        $country = $this->getElementProperty($element, 'telephone_validation_countries');
+      }
+      if ($format !== '') {
+        $element['#element_validate'][] = [
+          'Drupal\telephone_validation\Render\Element\TelephoneValidation',
+          'validateTel',
+        ];
+        $element['#element_validate_settings'] = [
+          'format' => $format,
+          'country' => $country,
+        ];
+      }
     }
   }
 
@@ -68,8 +117,8 @@ public function form(array $form, FormStateInterface $form_state) {
       '#type' => 'select',
       '#empty_option' => $this->t('- None -'),
       '#options' => [
-        'auto' => $this->t('Auto detect'),
-      ] + CountryManager::getStandardList(),
+          'auto' => $this->t('Auto detect'),
+        ] + CountryManager::getStandardList(),
       '#states' => [
         'visible' => [':input[name="properties[international]"]' => ['checked' => TRUE]],
       ],
@@ -79,6 +128,62 @@ public function form(array $form, FormStateInterface $form_state) {
       $form['telephone']['international']['#access'] = FALSE;
       $form['telephone']['international_initial_country']['#access'] = FALSE;
     }
+
+    // Add support for telephone_validation.module.
+    if (\Drupal::moduleHandler()->moduleExists('telephone_validation')) {
+      $form['telephone']['telephone_validation_format'] = [
+        '#type' => 'select',
+        '#title' => $this->t('Valid format'),
+        '#description' => $this->t('For international telephone numbers we suggest using <a href=":href">E164</a> format.', [':href' => 'https://en.wikipedia.org/wiki/E.164']),
+        '#empty_option' => $this->t('- None -'),
+        '#options' => [
+          \libphonenumber\PhoneNumberFormat::E164 => $this->t('E164'),
+          \libphonenumber\PhoneNumberFormat::NATIONAL => $this->t('National'),
+        ],
+      ];
+      $form['telephone']['telephone_validation_country'] = [
+        '#type' => 'select',
+        '#title' => $this->t('Valid country'),
+        '#options' => \Drupal::service('telephone_validation.validator')
+          ->getCountryList(),
+        '#states' => [
+          'visible' => [
+            ':input[name="properties[telephone_validation_format]"]' => ['value' => \libphonenumber\PhoneNumberFormat::NATIONAL],
+          ],
+          'required' => [
+            ':input[name="properties[telephone_validation_format]"]' => ['value' => \libphonenumber\PhoneNumberFormat::NATIONAL],
+          ],
+        ],
+      ];
+      $form['telephone']['telephone_validation_countries'] = [
+        '#type' => 'select',
+        '#title' => $this->t('Valid countries'),
+        '#description' => t('If no country selected all countries are valid.'),
+        '#options' => \Drupal::service('telephone_validation.validator')
+          ->getCountryList(),
+        '#select2' => TRUE,
+        '#multiple' => TRUE,
+        '#states' => [
+          'visible' => [
+            ':input[name="properties[telephone_validation_format]"]' => ['value' => \libphonenumber\PhoneNumberFormat::E164],
+          ],
+        ],
+      ];
+      $this->elementManager->processElement($form['telephone']['telephone_validation_countries']);
+    }
+    elseif (\Drupal::currentUser()->hasPermission('administer modules')) {
+      $t_args = [':href' => 'https://www.drupal.org/project/telephone_validation'];
+      $form['telephone']['telephone_validation_message'] = [
+        '#type' => 'webform_message',
+        '#message_type' => 'info',
+        '#message_message' => $this->t('Install the <a href=":href">Telephone validation</a> module which provides international phone number validation.', $t_args),
+        '#message_id' => 'webform.telephone_validation_message',
+        '#message_close' => TRUE,
+        '#message_storage' => WebformMessageElement::STORAGE_STATE,
+        '#access' => TRUE,
+      ];
+    }
+
     return $form;
   }
 
@@ -95,17 +200,6 @@ protected function formatHtmlItem(array $element, WebformSubmissionInterface $we
     $format = $this->getItemFormat($element);
     switch ($format) {
       case 'link':
-        /**********************************************************************/
-        // Issue #2484693: Telephone Link field formatter breaks Drupal with 5
-        // digits or less in the number
-        // return [
-        //   '#type' => 'link',
-        //   '#title' => $value,
-        //   '#url' => \Drupal::pathValidator()->getUrlIfValid('tel:' . $value),
-        // ];
-        // Workaround: Manually build a static HTML link.
-        /**********************************************************************/
-
         $t_args = [':tel' => 'tel:' . $value, '@tel' => $value];
         return t('<a href=":tel">@tel</a>', $t_args);
 
@@ -126,8 +220,8 @@ public function getItemDefaultFormat() {
    */
   public function getItemFormats() {
     return parent::getItemFormats() + [
-      'link' => $this->t('Link'),
-    ];
+        'link' => $this->t('Link'),
+      ];
   }
 
   /**
@@ -135,8 +229,8 @@ public function getItemFormats() {
    */
   public function preview() {
     return parent::preview() + [
-      '#international' => TRUE,
-    ];
+        '#international' => TRUE,
+      ];
   }
 
   /**
diff --git a/web/modules/webform/src/Plugin/WebformElement/TextBase.php b/web/modules/webform/src/Plugin/WebformElement/TextBase.php
index 5bfff9b95d7b5da984315c6410529a6d3ea911ad..f006e649f89c508ee48e7d3a9e9a3fa96c9beb3f 100644
--- a/web/modules/webform/src/Plugin/WebformElement/TextBase.php
+++ b/web/modules/webform/src/Plugin/WebformElement/TextBase.php
@@ -2,9 +2,9 @@
 
 namespace Drupal\webform\Plugin\WebformElement;
 
-use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\webform\Plugin\WebformElementBase;
+use Drupal\webform\Utility\WebformTextHelper;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
@@ -12,6 +12,8 @@
  */
 abstract class TextBase extends WebformElementBase {
 
+  use TextBaseTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -24,6 +26,7 @@ public function getDefaultProperties() {
       'placeholder' => '',
       'autocomplete' => 'on',
       'pattern' => '',
+      'pattern_error' => '',
     ] + parent::getDefaultProperties();
   }
 
@@ -31,7 +34,7 @@ public function getDefaultProperties() {
    * {@inheritdoc}
    */
   public function getTranslatableProperties() {
-    return array_merge(parent::getTranslatableProperties(), ['counter_message']);
+    return array_merge(parent::getTranslatableProperties(), ['counter_minimum_message', 'counter_maximum_message', 'pattern_error']);
   }
 
   /**
@@ -41,16 +44,32 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
     parent::prepare($element, $webform_submission);
 
     // Counter.
-    if (!empty($element['#counter_type']) && !empty($element['#counter_maximum']) && $this->librariesManager->isIncluded('jquery.word-and-character-counter')) {
+    if (!empty($element['#counter_type'])
+      && (!empty($element['#counter_minimum']) || !empty($element['#counter_maximum']))
+      && $this->librariesManager->isIncluded('jquery.textcounter')) {
 
+      // Apply character min/max to min/max length.
       if ($element['#counter_type'] == 'character') {
-        $element['#maxlength'] = $element['#counter_maximum'];
+        if (!empty($element['#counter_minimum'])) {
+          $element['#minlength'] = $element['#counter_minimum'];
+        }
+        if (!empty($element['#counter_maximum'])) {
+          $element['#maxlength'] = $element['#counter_maximum'];
+        }
       }
 
-      $element['#attributes']['data-counter-type'] = $element['#counter_type'];
-      $element['#attributes']['data-counter-limit'] = $element['#counter_maximum'];
-      if (!empty($element['#counter_message'])) {
-        $element['#attributes']['data-counter-message'] = $element['#counter_message'];
+      // Set 'data-counter-*' attributes using '#counter_*' properties.
+      $data_attributes = [
+        'counter_type',
+        'counter_minimum',
+        'counter_minimum_message',
+        'counter_maximum',
+        'counter_maximum_message',
+      ];
+      foreach ($data_attributes as $data_attribute) {
+        if (!empty($element['#' . $data_attribute])) {
+          $element['#attributes']['data-' . str_replace('_', '-', $data_attribute)] = $element['#' . $data_attribute];
+        }
       }
 
       $element['#attributes']['class'][] = 'js-webform-counter';
@@ -71,10 +90,38 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       else {
         $element['#attributes']['data-inputmask-mask'] = $input_mask;
       }
+      // Set input mask #pattern.
+      $input_masks = $this->getInputMasks();
+      if (isset($input_masks[$input_mask])
+        && isset($input_masks[$input_mask]['pattern']) &&
+        empty($element['#pattern'])) {
+        $element['#pattern'] = $input_masks[$input_mask]['pattern'];
+      }
 
       $element['#attributes']['class'][] = 'js-webform-input-mask';
       $element['#attached']['library'][] = 'webform/webform.element.inputmask';
     }
+
+    // Input hiding.
+    if (!empty($element['#input_hide'])) {
+      $element['#attributes']['class'][] = 'js-webform-input-hide';
+      $element['#attached']['library'][] = 'webform/webform.element.inputhide';
+    }
+
+    // Pattern validation.
+    // This override core's pattern validation to support unicode
+    // and a custom error message.
+    if (isset($element['#pattern'])) {
+      $element['#attributes']['pattern'] = $element['#pattern'];
+      $element['#element_validate'][] = [get_called_class(), 'validatePattern'];
+
+      // Set pattern error message using #pattern_error.
+      // @see Drupal.behaviors.webformRequiredError
+      // @see webform.form.js
+      if (!empty($element['#pattern_error'])) {
+        $element['#attributes']['data-webform-pattern-error'] = $element['#pattern_error'];
+      }
+    }
   }
 
   /**
@@ -88,33 +135,24 @@ public function form(array $form, FormStateInterface $form_state) {
       '#type' => 'webform_select_other',
       '#title' => $this->t('Input masks'),
       '#description' => $this->t('An <a href=":href">inputmask</a> helps the user with the element by ensuring a predefined format.', [':href' => 'https://github.com/RobinHerbots/jquery.inputmask']),
-      '#other__option_label' => $this->t('Custom...'),
-      '#other__placeholder' => $this->t('Enter input mask...'),
+      '#other__option_label' => $this->t('Custom…'),
+      '#other__placeholder' => $this->t('Enter input mask…'),
       '#other__description' => $this->t('(9 = numeric; a = alphabetical; * = alphanumeric)'),
       '#empty_option' => $this->t('- None -'),
-      '#options' => [
-        'Basic' => [
-          "'alias': 'currency'" => $this->t('Currency - @format', ['@format' => '$ 9.99']),
-          "'alias': 'mm/dd/yyyy'" => $this->t('Date - @format', ['@format' => 'mm/dd/yyyy']),
-          "'alias': 'decimal'" => $this->t('Decimal - @format', ['@format' => '1.234']),
-          "'alias': 'email'" => $this->t('Email - @format', ['@format' => 'example@example.com']),
-          "'alias': 'percentage'" => $this->t('Percentage - @format', ['@format' => '99%']),
-          '(999) 999-9999' => $this->t('Phone - @format', ['@format' => '(999) 999-9999']),
-          '99999[-9999]' => $this->t('Zip code - @format', ['@format' => '99999[-9999]']),
-        ],
-        'Advanced' => [
-          "'alias': 'ip'" => $this->t('IP address - @format', ['@format' => '255.255.255.255']),
-          '[9-]AAA-999' => $this->t('License plate - @format', ['@format' => '[9-]AAA-999']),
-          "'alias': 'mac'" => $this->t('MAC addresses - @format', ['@format' => '99-99-99-99-99-99']),
-          '999-99-9999' => $this->t('SSN - @format', ['@format' => '999-99-9999']),
-          "'alias': 'vin'" => 'VIN (Vehicle identification number)',
-        ],
-      ],
+      '#options' => $this->getInputMaskOptions(),
     ];
     if ($this->librariesManager->isExcluded('jquery.inputmask')) {
       $form['form']['input_mask']['#access'] = FALSE;
     }
 
+    // Input hiding.
+    $form['form']['input_hide'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Input hiding'),
+      '#description' => $this->t('Hide the input of the element when the input is not being focused.'),
+      '#return_value' => TRUE,
+    ];
+
     // Pattern.
     $form['validation']['pattern'] = [
       '#type' => 'webform_checkbox_value',
@@ -122,46 +160,19 @@ public function form(array $form, FormStateInterface $form_state) {
       '#description' => $this->t('A <a href=":href">regular expression</a> that the element\'s value is checked against.', [':href' => 'http://www.w3schools.com/js/js_regexp.asp']),
       '#value__title' => $this->t('Pattern regular expression'),
     ];
-
-    // Counter.
-    $form['validation']['counter_container'] = $this->getFormInlineContainer();
-    $form['validation']['counter_container']['counter_type'] = [
-      '#type' => 'select',
-      '#title' => $this->t('Count'),
-      '#description' => $this->t('Limit entered value to a maximum number of characters or words.'),
-      '#empty_option' => $this->t('- None -'),
-      '#options' => [
-        'character' => $this->t('Characters'),
-        'word' => $this->t('Words'),
-      ],
-    ];
-    $form['validation']['counter_container']['counter_maximum'] = [
-      '#type' => 'number',
-      '#title' => $this->t('Count maximum'),
-      '#min' => 1,
-      '#states' => [
-        'invisible' => [
-          ':input[name="properties[counter_type]"]' => ['value' => ''],
-        ],
-        'optional' => [
-          ':input[name="properties[counter_type]"]' => ['value' => ''],
-        ],
-      ],
-    ];
-    $form['validation']['counter_message'] = [
+    $form['validation']['pattern_error'] = [
       '#type' => 'textfield',
-      '#title' => $this->t('Count message'),
-      '#description' => $this->t('Defaults to: %value', ['%value' => $this->t('X characters/word(s) left')]),
+      '#title' => $this->t('Pattern message'),
+      '#description' => $this->t('If set, this message will be used when a pattern is not matched, instead of the default "@message" message.', ['@message' => t('%name field is not in the right format.')]),
       '#states' => [
-        'invisible' => [
-          ':input[name="properties[counter_type]"]' => ['value' => ''],
+        'visible' => [
+          ':input[name="properties[pattern][checkbox]"]' => ['checked' => TRUE],
         ],
       ],
     ];
-    if ($this->librariesManager->isExcluded('jquery.word-and-character-counter')) {
-      $form['validation']['counter_container']['#access'] = FALSE;
-      $form['validation']['counter_message']['#access'] = FALSE;
-    }
+
+    // Counter.
+    $form['validation'] += $this->buildCounterForm();
 
     if (isset($form['form']['maxlength'])) {
       $form['form']['maxlength']['#description'] .= ' ' . $this->t('If character counter is enabled, maxlength will automatically be set to the count maximum.');
@@ -176,41 +187,68 @@ public function form(array $form, FormStateInterface $form_state) {
   }
 
   /**
-   * Form API callback. Validate (word/charcter) counter.
+   * Form API callback. Validate (word/character) counter.
    */
   public static function validateCounter(array &$element, FormStateInterface $form_state) {
-    $name = $element['#name'];
-    $value = $form_state->getValue($name);
-    $type = $element['#counter_type'];
-    $max = $element['#counter_maximum'];
-
-    // Validate character count.
-    if ($type == 'character') {
-      $length = Unicode::strlen($value);
-      if ($length <= $max) {
-        return;
-      }
-    }
-    // Validate word count.
-    elseif ($type == 'word') {
-      $length = str_word_count($value);
-      if ($length <= $max) {
-        return;
-      }
-    }
-    else {
+    $value = $element['#value'];
+    if ($value === '') {
       return;
     }
 
+    $type = $element['#counter_type'];
+    $max = (!empty($element['#counter_maximum'])) ? $element['#counter_maximum'] : NULL;
+    $min = (!empty($element['#counter_minimum'])) ? $element['#counter_minimum'] : NULL;
+
     // Display error.
     // @see \Drupal\Core\Form\FormValidator::performRequiredValidation
     $t_args = [
+      '@type' => ($type == 'character') ? t('characters') : t('words'),
       '@name' => $element['#title'],
       '%max' => $max,
-      '@type' => ($type == 'character') ? t('characters') : t('words'),
-      '%length' => $length,
+      '%min' => $min,
     ];
-    $form_state->setError($element, t('@name cannot be longer than %max @type but is currently %length @type long.', $t_args));
+
+    // Get character/word count.
+    if ($type === 'character') {
+      $length = mb_strlen($value);
+      $t_args['%length'] = $length;
+    }
+    // Validate word count.
+    elseif ($type === 'word') {
+      $length = WebformTextHelper::wordCount($value);
+      $t_args['%length'] = $length;
+    }
+
+    // Validate character/word count.
+    if ($max && $length > $max) {
+      $form_state->setError($element, t('@name cannot be longer than %max @type but is currently %length @type long.', $t_args));
+    }
+    elseif ($min && $length < $min) {
+      $form_state->setError($element, t('@name must be longer than %min @type but is currently %length @type long.', $t_args));
+    }
+  }
+
+  /**
+   * Form API callback. Validate unicode pattern and display a custom error.
+   *
+   * @see https://www.drupal.org/project/drupal/issues/2633550
+   */
+  public static function validatePattern(&$element, FormStateInterface $form_state, &$complete_form) {
+    if ($element['#value'] !== '') {
+      // PHP: Convert JavaScript-escaped Unicode characters to PCRE
+      // escape sequence format.
+      // @see https://bytefreaks.net/programming-2/php-programming-2/php-convert-javascript-escaped-unicode-characters-to-html-hex-references˚
+      $pcre_pattern = preg_replace('/\\\\u([a-fA-F0-9]{4})/', '\\x{\\1}', $element['#pattern']);
+      $pattern = '{^(?:' . $pcre_pattern . ')$}u';
+      if (!preg_match($pattern, $element['#value'])) {
+        if (!empty($element['#pattern_error'])) {
+          $form_state->setError($element, $element['#pattern_error']);
+        }
+        else {
+          $form_state->setError($element, t('%name field is not in the right format.', ['%name' => $element['#title']]));
+        }
+      }
+    }
   }
 
   /**
@@ -225,16 +263,119 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
     // @see http://stackoverflow.com/questions/4440626/how-can-i-validate-regex
     if (!empty($properties['#pattern'])) {
       set_error_handler('_webform_entity_element_validate_rendering_error_handler');
-      if (preg_match('{^(?:' . $properties['#pattern'] . ')$}', NULL) === FALSE) {
+
+      // PHP: Convert JavaScript-escaped Unicode characters to PCRE escape
+      // sequence format.
+      // @see https://bytefreaks.net/programming-2/php-programming-2/php-convert-javascript-escaped-unicode-characters-to-html-hex-references
+      $pcre_pattern = preg_replace('/\\\\u([a-fA-F0-9]{4})/', '\\x{\\1}', $properties['#pattern']);
+
+      if (preg_match('{^(?:' . $pcre_pattern . ')$}u', NULL) === FALSE) {
         $form_state->setErrorByName('pattern', t('Pattern %pattern is not a valid regular expression.', ['%pattern' => $properties['#pattern']]));
       }
+
       set_error_handler('_drupal_error_handler');
     }
 
     // Validate #counter_maximum.
-    if (!empty($properties['#counter_type']) && empty($properties['#counter_maximum'])) {
-      $form_state->setErrorByName('counter_maximum', t('Counter maximum is required.'));
+    if (!empty($properties['#counter_type']) && empty($properties['#counter_maximum']) && empty($properties['#counter_minimum'])) {
+      $form_state->setErrorByName('counter_minimum', t('Counter minimum or maximum is required.'));
+      $form_state->setErrorByName('counter_maximum', t('Counter minimum or maximum is required.'));
+    }
+  }
+
+  /****************************************************************************/
+  // Input masks.
+  /****************************************************************************/
+
+  /**
+   * Get input masks.
+   *
+   * @return array
+   *   An associative array keyed my input mask contain input mask title,
+   *   example, and patterh.
+   */
+  protected function getInputMasks() {
+    return [
+      "'alias': 'currency'" => [
+        'title' => $this->t('Currency'),
+        'example' => '$ 9.99',
+        'pattern' => '^\$ [0-9]{1,3}(,[0-9]{3})*.\d\d$',
+      ],
+      "'alias': 'datetime'" => [
+        'title' => $this->t('Date'),
+        'example' => '2007-06-09\'T\'17:46:21',
+      ],
+      "'alias': 'decimal'" => [
+        'title' => $this->t('Decimal'),
+        'example' => '1.234',
+        'pattern' => '^\d+(\.\d+)?$',
+      ],
+      "'alias': 'email'" => [
+        'title' => $this->t('Email'),
+        'example' => 'example@example.com',
+      ],
+      "'alias': 'ip'" => [
+        'title' => $this->t('IP address'),
+        'example' => '255.255.255.255',
+        'pattern' => '^\d\d\d\.\d\d\d\.\d\d\d\.\d\d\d$',
+      ],
+      '[9-]AAA-999' => [
+        'title' => $this->t('License plate'),
+        'example' => '[9-]AAA-999',
+      ],
+      "'alias': 'mac'" => [
+        'title' => $this->t('MAC address'),
+        'example' => '99-99-99-99-99-99',
+        'pattern' => '^\d\d-\d\d-\d\d-\d\d-\d\d-\d\d$',
+      ],
+      "'alias': 'percentage'" => [
+        'title' => $this->t('Percentage'),
+        'example' => '99 %',
+        'pattern' => '^\d+ %$',
+      ],
+      '(999) 999-9999' => [
+        'title' => $this->t('Phone'),
+        'example' => '(999) 999-9999',
+        'pattern' => '^\(\d\d\d\) \d\d\d-\d\d\d\d$',
+      ],
+      '999-99-9999' => [
+        'title' => $this->t('Social Security Number (SSN)'),
+        'example' => '999-99-9999',
+        'pattern' => '^\d\d\d-\d\d-\d\d\d\d$',
+      ],
+      "'alias': 'vin'" => [
+        'title' => $this->t('Vehicle identification number (VIN)'),
+        'example' => 'JA3AY11A82U020534',
+      ],
+      '99999[-9999]' => [
+        'title' => $this->t('ZIP Code'),
+        'example' => '99999[-9999]',
+        'pattern' => '^\d\d\d\d\d(-\d\d\d\d)?$',
+      ],
+      "'casing': 'upper'" => [
+        'title' => $this->t('Uppercase'),
+        'example' => 'UPPERCASE',
+      ],
+      "'casing': 'lower'" => [
+        'title' => $this->t('Lowercase '),
+        'example' => 'lowercase',
+      ],
+    ];
+  }
+
+  /**
+   * Get input masks as select menu options.
+   *
+   * @return array
+   *   An associative array of options.
+   */
+  protected function getInputMaskOptions() {
+    $input_masks = $this->getInputMasks();
+    $options = [];
+    foreach ($input_masks as $input_mask => $settings) {
+      $options[$input_mask] = $settings['title'] . (!empty($settings['example']) ? ' - ' . $settings['example'] : '');
     }
+    return $options;
   }
 
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/TextBaseTrait.php b/web/modules/webform/src/Plugin/WebformElement/TextBaseTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..3e8b093b612b362b4d2e4f1439aa3fcdc15f0b74
--- /dev/null
+++ b/web/modules/webform/src/Plugin/WebformElement/TextBaseTrait.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Drupal\webform\Plugin\WebformElement;
+
+/**
+ * Text base trait contains methods that are applicable to any text elements.
+ */
+trait TextBaseTrait {
+
+  /**
+   * Build counter widget used by text elements and other element.
+   *
+   * @param string $name
+   *   Property name prefix.
+   * @param string $title
+   *   Property title prefix.
+   *
+   * @return array
+   *   A renderable array containing a counter configuration form.
+   *
+   * @see \Drupal\webform\Plugin\WebformElement\TextBase::form
+   * @see \Drupal\webform\Plugin\WebformElement\OptionsBase::form
+   */
+  public function buildCounterForm($name = '', $title = NULL) {
+    if ($title === NULL) {
+      $title = t('Counter');
+    }
+    $t_args = ['@title' => $title];
+
+    $build[$name . 'counter_type'] = [
+      '#type' => 'select',
+      '#title' => $title,
+      '#description' => $this->t('Limit entered value to a maximum number of characters or words.'),
+      '#empty_option' => $this->t('- None -'),
+      '#options' => [
+        'character' => $this->t('Characters'),
+        'word' => $this->t('Words'),
+      ],
+    ];
+    $build['counter_container'] = $this->getFormInlineContainer();
+    $build['counter_container']['#states'] = [
+      'invisible' => [
+        ':input[name="properties[' . $name . 'counter_type]"]' => ['value' => ''],
+      ],
+    ];
+    $build['counter_container'][$name . 'counter_minimum'] = [
+      '#type' => 'number',
+      '#title' => $this->t('@title minimum', $t_args),
+      '#min' => 1,
+      '#states' => [
+        'required' => [
+          ':input[name="properties[' . $name . 'counter_type]"]' => ['!value' => ''],
+          ':input[name="properties[' . $name . 'counter_maximum]"]' => ['value' => ''],
+        ],
+      ],
+    ];
+    $build['counter_container'][$name . 'counter_maximum'] = [
+      '#type' => 'number',
+      '#title' => $this->t('@title maximum', $t_args),
+      '#min' => 1,
+      '#states' => [
+        'required' => [
+          ':input[name="properties[' . $name . 'counter_type]"]' => ['!value' => ''],
+          ':input[name="properties[' . $name . 'counter_minimum]"]' => ['value' => ''],
+        ],
+      ],
+    ];
+    $build['counter_message_container'] = [
+      '#type' => 'container',
+      '#states' => [
+        'invisible' => [
+          ':input[name="properties[' . $name . 'counter_type]"]' => ['value' => ''],
+        ],
+      ],
+    ];
+    $build['counter_message_container'][$name . 'counter_minimum_message'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('@title minimum message', $t_args),
+      '#description' => $this->t('Defaults to: %value', ['%value' => $this->t('%d characters/word(s) entered')]),
+      '#states' => [
+        'visible' => [
+          ':input[name="properties[' . $name . 'counter_minimum]"]' => ['!value' => ''],
+          ':input[name="properties[' . $name . 'counter_maximum]"]' => ['value' => ''],
+        ],
+      ],
+    ];
+    $build['counter_message_container'][$name . 'counter_maximum_message'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('@title maximum message', $t_args),
+      '#description' => $this->t('Defaults to: %value', ['%value' => $this->t('%d characters/word(s) remaining')]),
+      '#states' => [
+        'visible' => [
+          ':input[name="properties[' . $name . 'counter_maximum]"]' => ['!value' => ''],
+        ],
+      ],
+    ];
+    if ($this->librariesManager->isExcluded('jquery.textcounter')) {
+      $build[$name . 'counter_type']['#access'] = FALSE;
+      $build[$name . 'counter_container']['#access'] = FALSE;
+      $build[$name . 'counter_message_container']['#access'] = FALSE;
+    }
+    return $build;
+  }
+
+}
diff --git a/web/modules/webform/src/Plugin/WebformElement/TextField.php b/web/modules/webform/src/Plugin/WebformElement/TextField.php
index cecb900dec806d7f57b609c0eb478936647a1e7f..7618bfcd5a72c506646651522e071fa25d26e717 100644
--- a/web/modules/webform/src/Plugin/WebformElement/TextField.php
+++ b/web/modules/webform/src/Plugin/WebformElement/TextField.php
@@ -24,10 +24,13 @@ public function getDefaultProperties() {
     return [
       // Form display.
       'input_mask' => '',
+      'input_hide' => FALSE,
       // Form validation.
       'counter_type' => '',
+      'counter_minimum' => '',
+      'counter_minimum_message' => '',
       'counter_maximum' => '',
-      'counter_message' => '',
+      'counter_maximum_message' => '',
     ] + parent::getDefaultProperties() + $this->getDefaultMultipleProperties();
   }
 
@@ -35,7 +38,9 @@ public function getDefaultProperties() {
    * {@inheritdoc}
    */
   public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
-    $element['#maxlength'] = (!isset($element['#maxlength'])) ? 255 : $element['#maxlength'];
+    if (!array_key_exists('#maxlength', $element)) {
+      $element['#maxlength'] = 255;
+    }
     parent::prepare($element, $webform_submission);
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/TextFormat.php b/web/modules/webform/src/Plugin/WebformElement/TextFormat.php
index a8f6fc8a1552eac517bb7ffbfbe5b5d65267acf9..fcaee819c0ac09a669e030495660fea191adbaad 100644
--- a/web/modules/webform/src/Plugin/WebformElement/TextFormat.php
+++ b/web/modules/webform/src/Plugin/WebformElement/TextFormat.php
@@ -242,10 +242,54 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
   }
 
   /**
-   * Get composite element.
+   * {@inheritdoc}
+   */
+  public function postSave(array &$element, WebformSubmissionInterface $webform_submission, $update = TRUE) {
+    $webform = $webform_submission->getWebform();
+    if ($webform->isResultsDisabled()) {
+      return;
+    }
+
+    // Get current value and original value for this element.
+    $key = $element['#webform_key'];
+
+    $data = $webform_submission->getData();
+    $value = (isset($data[$key]) && isset($data[$key]['value'])) ? $data[$key]['value'] : '';
+    $uuids = _webform_parse_file_uuids($value);
+
+    if ($update) {
+      $original_data = $webform_submission->getOriginalData();
+      $original_value = isset($original_data[$key]) ? $original_data[$key]['value'] : '';
+      $original_uuids = _webform_parse_file_uuids($original_value);
+
+      // Detect file usages that should be incremented.
+      $added_files = array_diff($uuids, $original_uuids);
+      _webform_record_file_usage($added_files, $webform_submission->getEntityTypeId(), $webform_submission->id());
+
+      // Detect file usages that should be decremented.
+      $removed_files = array_diff($original_uuids, $uuids);
+      _webform_delete_file_usage($removed_files, $webform_submission->getEntityTypeId(), $webform_submission->id(), 1);
+    }
+    else {
+      _webform_record_file_usage($uuids, $webform_submission->getEntityTypeId(), $webform_submission->id());
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postDelete(array &$element, WebformSubmissionInterface $webform_submission) {
+    $key = $element['#webform_key'];
+    $value = $webform_submission->getElementData($key);
+    $uuids = _webform_parse_file_uuids($value['value']);
+    _webform_delete_file_usage($uuids, $webform_submission->getEntityTypeId(), $webform_submission->id(), 0);
+  }
+
+  /**
+   * Check if composite element exists.
    *
-   * @return array
-   *   A composite sub-elements.
+   * @return bool
+   *   TRUE if composite element exists.
    */
   public function hasCompositeElement(array $element, $key) {
     $elements = $this->getCompositeElements();
diff --git a/web/modules/webform/src/Plugin/WebformElement/Textarea.php b/web/modules/webform/src/Plugin/WebformElement/Textarea.php
index 35d92aa32f838565b1837f771254ef613c25de83..1eb1bce8c18a7f567804f259fe594a6007761661 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Textarea.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Textarea.php
@@ -29,6 +29,7 @@ public function getDefaultProperties() {
       'default_value' => '',
       // Description/Help.
       'help' => '',
+      'help_title' => '',
       'description' => '',
       'more' => '',
       'more_title' => '',
@@ -50,10 +51,13 @@ public function getDefaultProperties() {
       'unique_entity' => FALSE,
       'unique_error' => '',
       'counter_type' => '',
+      'counter_minimum' => '',
+      'counter_minimum_message' => '',
       'counter_maximum' => '',
-      'counter_message' => '',
+      'counter_maximum_message' => '',
       // Attributes.
       'wrapper_attributes' => [],
+      'label_attributes' => [],
       'attributes' => [],
       // Submission display.
       'format' => $this->getItemDefaultFormat(),
@@ -62,23 +66,10 @@ public function getDefaultProperties() {
       'format_items' => $this->getItemsDefaultFormat(),
       'format_items_html' => '',
       'format_items_text' => '',
+      'format_attributes' => [],
     ] + parent::getDefaultProperties() + $this->getDefaultMultipleProperties();
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
-    parent::prepare($element, $webform_submission);
-
-    // @todo Remove once Drupal 8.4.x+ is a dependency.
-    // Textarea Form API element now supports #maxlength attribute
-    // @see https://www.drupal.org/node/2887280
-    if (!empty($element['#maxlength'])) {
-      $element['#attributes']['maxlength'] = $element['#maxlength'];
-    }
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/web/modules/webform/src/Plugin/WebformElement/Url.php b/web/modules/webform/src/Plugin/WebformElement/Url.php
index 97f955ba81de64811bcdff39583751573e0c7507..d22e943d4e7fd818f52de2b9f7e5cbfcca4671ba 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Url.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Url.php
@@ -21,7 +21,10 @@ class Url extends TextBase {
    * {@inheritdoc}
    */
   public function getDefaultProperties() {
-    return parent::getDefaultProperties() + $this->getDefaultMultipleProperties();
+    return [
+      'input_hide' => FALSE,
+    ] + parent::getDefaultProperties()
+      + $this->getDefaultMultipleProperties();
   }
 
   /**
diff --git a/web/modules/webform/src/Plugin/WebformElement/Value.php b/web/modules/webform/src/Plugin/WebformElement/Value.php
index 3eaa177d43c4bbae2d2c55e2848c1928a015b6de..fbc84f30f45b18aa94da4c58946ef125b5028a53 100644
--- a/web/modules/webform/src/Plugin/WebformElement/Value.php
+++ b/web/modules/webform/src/Plugin/WebformElement/Value.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\webform\Plugin\WebformElement;
 
+use Drupal\webform\WebformInterface;
+
 /**
  * Provides a 'value' element.
  *
@@ -40,4 +42,12 @@ public function getElementSelectorOptions(array $element) {
     return [];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getTestValues(array $element, WebformInterface $webform, array $options = []) {
+    // Value elements should never get a test value.
+    return NULL;
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformActions.php b/web/modules/webform/src/Plugin/WebformElement/WebformActions.php
index 27a867509cacb21d807765c47147a2e5bfc4b108..b604b32f6c5c8621b3c22285fc01b3398df3f751 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformActions.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformActions.php
@@ -29,9 +29,7 @@ public function getDefaultProperties() {
       'title' => '',
       // Attributes.
       'attributes' => [],
-      // Conditional logic.
-      'states' => [],
-    ];
+    ] + $this->getDefaultBaseProperties();
     foreach (WebformActionsElement::$buttons as $button) {
       $properties[$button . '_hide'] = FALSE;
       $properties[$button . '__label'] = '';
@@ -226,11 +224,17 @@ public function form(array $form, FormStateInterface $form_state) {
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
     $form = parent::buildConfigurationForm($form, $form_state);
 
-    /** @var \Drupal\webform\WebformInterface $webform */
-    $webform = $form_state->getFormObject()->getWebform();
+    /** @var \Drupal\webform_ui\Form\WebformUiElementEditForm $form_object */
+    $form_object = $form_state->getFormObject();
+
+    if (!$form_object->getWebform()->hasActions()) {
+      $form['element']['title']['#default_value'] = (string) $this->t('Submit button(s)');
+    }
 
-    if (!$webform->hasActions()) {
-      $form['element']['title']['#default_value'] = $this->t('Submit button(s)');
+    // Hide element settings for default 'actions' to prevent UX confusion.
+    $key = $form_object->getKey() ?: $form_object->getDefaultKey();
+    if ($key === 'actions') {
+      $form['element']['#access'] = FALSE;
     }
 
     return $form;
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformAddress.php b/web/modules/webform/src/Plugin/WebformElement/WebformAddress.php
index 0b0d37b95139fc7c0948f7d3fc14b036496c8aba..d08d9bfc3aabd30da6fea418cf5b439b11c510b2 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformAddress.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformAddress.php
@@ -19,6 +19,13 @@
  */
 class WebformAddress extends WebformCompositeBase {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginLabel() {
+    return \Drupal::moduleHandler()->moduleExists('address') ? $this->t('Basic address') : parent::getPluginLabel();
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformCheckboxesOther.php b/web/modules/webform/src/Plugin/WebformElement/WebformCheckboxesOther.php
index bcfac7d912512c220eac5f818ee7c536eef4fd61..aa8298956896a5cb12a96dfe259184c4674e7cf7 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformCheckboxesOther.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformCheckboxesOther.php
@@ -3,6 +3,7 @@
 namespace Drupal\webform\Plugin\WebformElement;
 
 use Drupal\webform\WebformSubmissionInterface;
+use Drupal\webform\WebformSubmissionConditionsValidator;
 
 /**
  * Provides a 'checkboxes_other' element.
@@ -16,14 +17,6 @@
  */
 class WebformCheckboxesOther extends Checkboxes implements WebformOtherInterface {
 
-  /**
-   * {@inheritdoc}
-   */
-  public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
-    $element['#element_validate'][] = [get_class($this), 'validateMultipleOptions'];
-    parent::prepare($element, $webform_submission);
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -39,4 +32,26 @@ public function getElementSelectorOptions(array $element) {
     return [$title => $selectors];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorInputValue($selector, $trigger, array $element, WebformSubmissionInterface $webform_submission) {
+    $input_name = WebformSubmissionConditionsValidator::getSelectorInputName($selector);
+    $other_type = WebformSubmissionConditionsValidator::getInputNameAsArray($input_name, 1);
+    $value = $this->getRawValue($element, $webform_submission) ?: [];
+    if ($other_type === 'other') {
+      $other_value = array_diff($value, array_keys($element['#options']));
+      return ($other_value) ? implode(', ', $other_value) : NULL;
+    }
+    else {
+      $option_value = WebformSubmissionConditionsValidator::getInputNameAsArray($input_name, 2);
+      if (in_array($option_value, $value, TRUE)) {
+        return (in_array($trigger, ['checked', 'unchecked'])) ? TRUE : $value;
+      }
+      else {
+        return (in_array($trigger, ['checked', 'unchecked'])) ? FALSE : NULL;
+      }
+    }
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformCodeMirror.php b/web/modules/webform/src/Plugin/WebformElement/WebformCodeMirror.php
index 5767c7aa75843212b2a055f5aa1a39db9d355189..51218c6019477c13bb31fd2d0c3cfcf7d49f52f4 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformCodeMirror.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformCodeMirror.php
@@ -28,6 +28,7 @@ public function getDefaultProperties() {
       // Codemirror settings.
       'placeholder' => '',
       'mode' => 'text',
+      'wrap' => TRUE,
     ] + parent::getDefaultProperties();
   }
 
@@ -130,6 +131,11 @@ public function form(array $form, FormStateInterface $form_state) {
       ],
       '#required' => TRUE,
     ];
+    $form['codemirror']['wrap'] = [
+      '#title' => $this->t('Wrap long lines of text'),
+      '#type' => 'checkbox',
+      '#return_value' => TRUE,
+    ];
     return $form;
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformCompositeBase.php b/web/modules/webform/src/Plugin/WebformElement/WebformCompositeBase.php
index b867c292317472117996fa47361d170adcb63dfa..3eef7b76a078e5d295e4babeea068667f400b5d3 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformCompositeBase.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformCompositeBase.php
@@ -3,6 +3,7 @@
 namespace Drupal\webform\Plugin\WebformElement;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\OptGroup;
 use Drupal\Core\Render\Element as RenderElement;
 use Drupal\Core\Render\Element;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
@@ -37,6 +38,13 @@ abstract class WebformCompositeBase extends WebformElementBase {
    */
   protected $initializedCompositeElement;
 
+  /**
+   * Track managed file elements.
+   *
+   * @var array
+   */
+  protected $elementsManagedFiles;
+
   /****************************************************************************/
   // Property methods.
   /****************************************************************************/
@@ -46,32 +54,17 @@ abstract class WebformCompositeBase extends WebformElementBase {
    */
   public function getDefaultProperties() {
     $properties = [
-      'title' => '',
       'default_value' => [],
-      // Description/Help.
-      'help' => '',
-      'description' => '',
-      'more' => '',
-      'more_title' => '',
-      // Form display.
-      'description_display' => '',
       'title_display' => 'invisible',
       'disabled' => FALSE,
-      // Form validation.
-      'required' => FALSE,
-      'required_error' => '',
-      // Flex box.
       'flexbox' => '',
-      // Attributes.
-      'wrapper_attributes' => [],
-      // Submission display.
-      'format' => $this->getItemDefaultFormat(),
-      'format_html' => '',
-      'format_text' => '',
-      'format_items' => $this->getItemsDefaultFormat(),
-      'format_items_html' => '',
-      'format_items_text' => '',
+      // Enhancements.
+      'select2' => FALSE,
+      'chosen' => FALSE,
+      // Wrapper.
+      'wrapper_type' => 'fieldset',
     ] + parent::getDefaultProperties() + $this->getDefaultMultipleProperties();
+    unset($properties['required_error']);
 
     $composite_elements = $this->getCompositeElements();
     foreach ($composite_elements as $composite_key => $composite_element) {
@@ -88,6 +81,7 @@ public function getDefaultProperties() {
         }
       }
       if (isset($properties[$composite_key . '__type'])) {
+        $properties[$composite_key . '__title_display'] = '';
         $properties[$composite_key . '__description'] = '';
         $properties[$composite_key . '__help'] = '';
         $properties[$composite_key . '__required'] = FALSE;
@@ -108,6 +102,13 @@ protected function getDefaultMultipleProperties() {
     ] + parent::getDefaultMultipleProperties();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function hasManagedFiles(array $element) {
+    return ($this->getManagedFiles($element)) ? TRUE : FALSE;
+  }
+
   /****************************************************************************/
   // Element relationship methods.
   /****************************************************************************/
@@ -267,25 +268,34 @@ public function getElementSelectorOptions(array $element) {
       $has_access = (!isset($composite_elements['#access']) || $composite_elements['#access']);
       if ($has_access && isset($composite_element['#type'])) {
         $element_plugin = $this->elementManager->getElementInstance($composite_element);
-        $composite_title = (isset($composite_element['#title'])) ? $composite_element['#title'] : $composite_key;
+        $composite_element['#webform_key'] = "{$name}[{$composite_key}]";
+        $selectors += OptGroup::flattenOptions($element_plugin->getElementSelectorOptions($composite_element));
+      }
+    }
+    return [$title => $selectors];
+  }
 
-        switch ($composite_element['#type']) {
-          case 'label':
-          case 'webform_message':
-            break;
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorSourceValues(array $element) {
+    if ($this->hasMultipleValues($element)) {
+      return [];
+    }
 
-          case 'webform_select_other':
-            $selectors[":input[name=\"{$name}[{$composite_key}][select]\"]"] = $composite_title . ' [' . $this->t('Select') . ']';
-            $selectors[":input[name=\"{$name}[{$composite_key}][other]\"]"] = $composite_title . ' [' . $this->t('Textfield') . ']';
-            break;
+    $name = $element['#webform_key'];
 
-          default:
-            $selectors[":input[name=\"{$name}[{$composite_key}]\"]"] = $composite_title . ' [' . $element_plugin->getPluginLabel() . ']';
-            break;
-        }
+    $source_values = [];
+    $composite_elements = $this->getInitializedCompositeElement($element);
+    foreach ($composite_elements as $composite_key => $composite_element) {
+      $has_access = (!isset($composite_elements['#access']) || $composite_elements['#access']);
+      if ($has_access && isset($composite_element['#type'])) {
+        $element_plugin = $this->elementManager->getElementInstance($composite_element);
+        $composite_element['#webform_key'] = "{$name}[{$composite_key}]";
+        $source_values += $element_plugin->getElementSelectorSourceValues($composite_element);
       }
     }
-    return [$title => $selectors];
+    return $source_values;
   }
 
   /****************************************************************************/
@@ -319,7 +329,7 @@ public function formatText(array $element, WebformSubmissionInterface $webform_s
   /**
    * {@inheritdoc}
    */
-  protected function formatCustomItem($type, array &$element, WebformSubmissionInterface $webform_submission, array $options = []) {
+  protected function formatCustomItem($type, array &$element, WebformSubmissionInterface $webform_submission, array $options = [], array $context = []) {
     $name = strtolower($type);
 
     // Parse element.composite_key from template and add composite element
@@ -330,17 +340,15 @@ protected function formatCustomItem($type, array &$element, WebformSubmissionInt
       $composite_keys = array_unique($matches[1]);
 
       $item_function = 'format' . $type;
-      $options['context'] = [
-        'element' => [],
-      ];
+      $context['element'] = [];
       foreach ($composite_keys as $composite_key) {
         if (isset($composite_elements[$composite_key])) {
-          $options['context']['element'][$composite_key] = $this->$item_function(['#format' => NULL] + $element, $webform_submission, ['composite_key' => $composite_key] + $options);
+          $context['element'][$composite_key] = $this->$item_function(['#format' => NULL] + $element, $webform_submission, ['composite_key' => $composite_key] + $options);
         }
       }
     }
 
-    return parent::formatCustomItem($type, $element, $webform_submission, $options);
+    return parent::formatCustomItem($type, $element, $webform_submission, $options, $context);
   }
 
   /**
@@ -352,6 +360,12 @@ protected function formatHtmlItem(array $element, WebformSubmissionInterface $we
     }
 
     $format = $this->getItemFormat($element);
+
+    // Handle custom composite html items.
+    if ($format === 'custom' && !empty($element['#format_html'])) {
+      return $this->formatCustomItem('html', $element, $webform_submission, $options);
+    }
+
     switch ($format) {
       case 'list':
       case 'raw':
@@ -363,6 +377,9 @@ protected function formatHtmlItem(array $element, WebformSubmissionInterface $we
 
       default:
         $lines = $this->formatHtmlItemValue($element, $webform_submission, $options);
+        if (empty($lines)) {
+          return '';
+        }
         foreach ($lines as $key => $line) {
           if (is_string($line)) {
             $lines[$key] = ['#markup' => $line];
@@ -435,7 +452,7 @@ protected function formatHtmlItems(array &$element, WebformSubmissionInterface $
   protected function formatTextItems(array &$element, WebformSubmissionInterface $webform_submission, array $options = []) {
     $format = $this->getItemsFormat($element);
     if ($format === 'table') {
-       $element['#format_items'] = 'hr';
+      $element['#format_items'] = 'hr';
     }
     return parent::formatTextItems($element, $webform_submission, $options);
   }
@@ -449,6 +466,12 @@ protected function formatTextItem(array $element, WebformSubmissionInterface $we
     }
 
     $format = $this->getItemFormat($element);
+
+    // Handle custom composite text items.
+    if ($format === 'custom' && !empty($element['#format_text'])) {
+      return $this->formatCustomItem('text', $element, $webform_submission, $options);
+    }
+
     switch ($format) {
       case 'list':
       case 'raw':
@@ -765,6 +788,11 @@ public function getTestValues(array $element, WebformInterface $webform, array $
 
     $values = [];
     for ($i = 1; $i <= 3; $i++) {
+      // Add delta to $options to allow multiple unique managed test files
+      // to be created.
+      // @see \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::getTestValues
+      $options['delta'] = $i;
+
       $value = [];
       foreach (RenderElement::children($composite_elements) as $composite_key) {
         $value[$composite_key] = $generate->getTestValue($webform, $composite_key, $composite_elements[$composite_key], $options);
@@ -798,7 +826,8 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['default']['default_value']['#description'] = $this->t("The default value of the composite webform element as YAML.");
 
     // Update #required label.
-    $form['validation']['required_container']['required']['#description'] .= '<br /><br />' . $this->t("Checking this option only displays the required indicator next to this element's label. Please chose which elements should be required.");
+    $form['validation']['required_container']['required']['#title'] .= ' <em>' . $this->t('(Display purposes only)') . '</em>';
+    $form['validation']['required_container']['required']['#description'] = $this->t('If checked, adds required indicator to the title, if visible. To enforce individual fields, also tick "Required" under the @name settings above.', ['@name' => $this->getPluginLabel()]);
 
     // Update '#multiple__header_label'.
     $form['element']['multiple__header_container']['multiple__header_label']['#states']['visible'][':input[name="properties[multiple__header]"]'] = ['checked' => FALSE];
@@ -820,12 +849,6 @@ public function form(array $form, FormStateInterface $form_state) {
       ],
     ];
 
-    $item_pattern = &$form['display']['item']['patterns']['#value']['items']['#items'];
-    $composite_elements = $this->getCompositeElements();
-    foreach ($composite_elements as $composite_key => $composite_element) {
-      $item_pattern[] = "{{ element.$composite_key }}";
-    }
-
     // Hide single item display when multiple item display is set to 'table'.
     $form['display']['item']['#states']['invisible'] = [
       ':input[name="properties[format_items]"]' => ['value' => 'table'],
@@ -833,6 +856,45 @@ public function form(array $form, FormStateInterface $form_state) {
 
     $form['#attached']['library'][] = 'webform/webform.admin.composite';
 
+    // Select2 and/or Chosen enhancements.
+    // @see \Drupal\webform\Plugin\WebformElement\Select::form
+    $form['composite']['select2'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Select2'),
+      '#description' => $this->t('Replace select element with jQuery <a href=":href">Select2</a> box.', [':href' => 'https://select2.github.io/']),
+      '#return_value' => TRUE,
+      '#states' => [
+        'disabled' => [
+          ':input[name="properties[chosen]"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    if ($this->librariesManager->isExcluded('jquery.select2')) {
+      $form['composite']['select2']['#access'] = FALSE;
+    }
+    $form['composite']['chosen'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Chosen'),
+      '#description' => $this->t('Replace select element with jQuery <a href=":href">Chosen</a> box.', [':href' => 'https://harvesthq.github.io/chosen/']),
+      '#return_value' => TRUE,
+      '#states' => [
+        'disabled' => [
+          ':input[name="properties[select2]"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    if ($this->librariesManager->isExcluded('jquery.chosen')) {
+      $form['composite']['chosen']['#access'] = FALSE;
+    }
+    if ($this->librariesManager->isIncluded('jquery.select2') && $this->librariesManager->isIncluded('jquery.chosen')) {
+      $form['composite']['select_message'] = [
+        '#type' => 'webform_message',
+        '#message_type' => 'warning',
+        '#message_message' => $this->t('Select2 and Chosen provide very similar functionality, only one should be enabled.'),
+        '#access' => TRUE,
+      ];
+    }
+
     return $form;
   }
 
@@ -843,12 +905,42 @@ public function form(array $form, FormStateInterface $form_state) {
    *   A renderable array container the composite elements settings table.
    */
   protected function buildCompositeElementsTable() {
+    $labels_help = [
+      'help' => [
+        '#type' => 'webform_help',
+        '#help' => '<b>' . t('Key') . ':</b> ' . t('The machine-readable name.') .
+          '<hr/><b>' . t('Title') . ':</b> ' . t('This is used as a descriptive label when displaying this webform element.') .
+          '<hr/><b>' . t('Placeholder') . ':</b> ' . t('The placeholder will be shown in the element until the user starts entering a value.') .
+          '<hr/><b>' . t('Description') . ':</b> ' . t('A short description of the element used as help for the user when he/she uses the webform.') .
+          '<hr/><b>' . t('Help text') . ':</b> ' . t('A tooltip displayed after the title.') .
+          '<hr/><b>' . t('Title display') . ':</b> ' . t('A tooltip displayed after the title.'),
+        '#help_title' => $this->t('Labels'),
+      ],
+    ];
+    $settings_help = [
+      'help' => [
+        '#type' => 'webform_help',
+        '#help' => '<b>' . t('Required') . ':</b> ' . t('Check this option if the user must enter a value.') .
+          '<hr/><b>' . t('Type') . ':</b> ' . t('The type of element to be displayed.') .
+          '<hr/><b>' . t('Options') . ':</b> ' . t('Please select predefined options.'),
+        '#help_title' => $this->t('Settings'),
+      ],
+    ];
+
     $header = [
-      'key' => $this->t('Key'),
-      'title_placeholder_help_description' => $this->t('Title / Placeholder / Help / Description'),
-      'type_options' => $this->t('Type/Options'),
-      'required' => $this->t('Required'),
       'visible' => $this->t('Visible'),
+      'labels' => [
+        'data' => [
+          ['title' => ['#markup' => $this->t('Labels')]],
+          $labels_help,
+        ],
+      ],
+      'settings' => [
+        'data' => [
+          ['title' => ['#markup' => $this->t('Settings')]],
+          $settings_help,
+        ],
+      ],
     ];
 
     $rows = [];
@@ -867,44 +959,75 @@ protected function buildCompositeElementsTable() {
 
       $row = [];
 
-      // Key.
-      $row[$composite_key . '__key'] = [
-        '#markup' => $composite_key,
-        '#access' => TRUE,
+      // Access.
+      $row[$composite_key . '__access'] = [
+        '#title' => $this->t('@title visible', $t_args),
+        '#title_display' => 'invisible',
+        '#type' => 'checkbox',
+        '#return_value' => TRUE,
       ];
 
-      // Title, placeholder, help, description.
+      // Key, title, placeholder, help, description, and title display.
       if ($type) {
-        $row['title_placeholder_help_description'] = [
+        $row['labels'] = [
           'data' => [
+            $composite_key . '__key' => [
+              '#markup' => $composite_key,
+              '#suffix' => '<hr/>',
+              '#access' => TRUE,
+            ],
             $composite_key . '__title' => [
               '#type' => 'textfield',
               '#title' => $this->t('@title title', $t_args),
               '#title_display' => 'invisible',
-              '#placeholder' => $this->t('Enter title...'),
+              '#description' => t('This is used as a descriptive label when displaying this webform element.'),
+              '#description_display' => 'invisible',
+              '#placeholder' => $this->t('Enter title…'),
+              '#required' => TRUE,
               '#states' => $state_disabled,
             ],
             $composite_key . '__placeholder' => [
               '#type' => 'textfield',
               '#title' => $this->t('@title placeholder', $t_args),
               '#title_display' => 'invisible',
-              '#placeholder' => $this->t('Enter placeholder...'),
+              '#description' => t('The placeholder will be shown in the element until the user starts entering a value.'),
+              '#description_display' => 'invisible',
+              '#placeholder' => $this->t('Enter placeholder…'),
               '#states' => $state_disabled,
             ],
             $composite_key . '__help' => [
               '#type' => 'textarea',
               '#title' => $this->t('@title help text', $t_args),
               '#title_display' => 'invisible',
+              '#description' => t('A short description of the element used as help for the user when he/she uses the webform.'),
+              '#description_display' => 'invisible',
               '#rows' => 2,
-              '#placeholder' => $this->t('Enter help text...'),
+              '#placeholder' => $this->t('Enter help text…'),
               '#states' => $state_disabled,
             ],
             $composite_key . '__description' => [
               '#type' => 'textarea',
               '#title' => $this->t('@title description', $t_args),
               '#title_display' => 'invisible',
+              '#description' => t('A tooltip displayed after the title.'),
+              '#description_display' => 'invisible',
               '#rows' => 2,
-              '#placeholder' => $this->t('Enter description...'),
+              '#placeholder' => $this->t('Enter description…'),
+              '#states' => $state_disabled,
+            ],
+            $composite_key . '__title_display' => [
+              '#type' => 'select',
+              '#title' => $this->t('@title title display', $t_args),
+              '#title_display' => 'invisible',
+              '#description' => t('A tooltip displayed after the title.'),
+              '#description_display' => 'invisible',
+              '#options' => [
+                'before' => $this->t('Before'),
+                'after' => $this->t('After'),
+                'inline' => $this->t('Inline'),
+                'invisible' => $this->t('Invisible'),
+              ],
+              '#empty_option' => $this->t('Select title display… '),
               '#states' => $state_disabled,
             ],
           ],
@@ -916,10 +1039,29 @@ protected function buildCompositeElementsTable() {
 
       // Type and options.
       // Using if/else instead of switch/case because of complex conditions.
-      $row['type_options'] = [];
+      $row['settings'] = [];
+
+      // Required.
+      if ($type) {
+        $row['settings']['data'][$composite_key . '__required'] = [
+          '#type' => 'checkbox',
+          '#title' => $this->t('Required'),
+          '#description' => t('Check this option if the user must enter a value.'),
+          '#description_display' => 'invisible',
+          '#return_value' => TRUE,
+          '#states' => $state_disabled,
+          '#wrapper_attributes' => ['style' => 'white-space: nowrap'],
+          '#suffix' => '<hr/>',
+        ];
+      }
+
       if ($type == 'tel') {
-        $row['type_options']['data'][$composite_key . '__type'] = [
+        $row['settings']['data'][$composite_key . '__type'] = [
           '#type' => 'select',
+          '#title' => $this->t('@title type', $t_args),
+          '#title_display' => 'invisible',
+          '#description' => t('The type of element to be displayed.'),
+          '#description_display' => 'invisible',
           '#required' => TRUE,
           '#options' => [
             'tel' => $this->t('Telephone'),
@@ -935,7 +1077,7 @@ protected function buildCompositeElementsTable() {
         // Get type options.
         switch ($base_type) {
           case 'radios':
-            $type_options = [
+            $settings = [
               'radios' => $this->t('Radios'),
               'webform_radios_other' => $this->t('Radios other'),
               'textfield' => $this->t('Text field'),
@@ -944,7 +1086,7 @@ protected function buildCompositeElementsTable() {
 
           case 'select':
           default:
-            $type_options = [
+            $settings = [
               'select' => $this->t('Select'),
               'webform_select_other' => $this->t('Select other'),
               'textfield' => $this->t('Text field'),
@@ -952,15 +1094,23 @@ protected function buildCompositeElementsTable() {
             break;
         }
 
-        $row['type_options']['data'][$composite_key . '__type'] = [
+        $row['settings']['data'][$composite_key . '__type'] = [
           '#type' => 'select',
+          '#title' => $this->t('@title type', $t_args),
+          '#title_display' => 'invisible',
+          '#description' => t('The type of element to be displayed.'),
+          '#description_display' => 'invisible',
           '#required' => TRUE,
-          '#options' => $type_options,
+          '#options' => $settings,
           '#states' => $state_disabled,
         ];
         if ($composite_options = $this->getCompositeElementOptions($composite_key)) {
-          $row['type_options']['data'][$composite_key . '__options'] = [
+          $row['settings']['data'][$composite_key . '__options'] = [
             '#type' => 'select',
+            '#title' => $this->t('@title options', $t_args),
+            '#title_display' => 'invisible',
+            '#description' => t('Please select predefined options.'),
+            '#description_display' => 'invisible',
             '#options' => $composite_options,
             '#required' => TRUE,
             '#attributes' => ['style' => 'width: 100%;'],
@@ -974,39 +1124,23 @@ protected function buildCompositeElementsTable() {
           ];
         }
         else {
-          $row['type_options']['data'][$composite_key . '__options'] = [
+          $row['settings']['data'][$composite_key . '__options'] = [
             '#type' => 'value',
           ];
         }
       }
       else {
-        $row['type_options']['data'][$composite_key . '__type'] = [
+        $row['settings']['data'][$composite_key . '__type'] = [
           '#type' => 'textfield',
           '#access' => FALSE,
         ];
-        $row['type_options']['data']['markup'] = [
+        $row['settings']['data']['markup'] = [
+          '#type' => 'container',
           '#markup' => $this->elementManager->getElementInstance($composite_element)->getPluginLabel(),
           '#access' => TRUE,
         ];
       }
 
-      // Required.
-      if ($type) {
-        $row[$composite_key . '__required'] = [
-          '#type' => 'checkbox',
-          '#return_value' => TRUE,
-        ];
-      }
-      else {
-        $row[$composite_key . '__required'] = ['data' => ['']];
-      }
-
-      // Access.
-      $row[$composite_key . '__access'] = [
-        '#type' => 'checkbox',
-        '#return_value' => TRUE,
-      ];
-
       $rows[$composite_key] = $row;
     }
 
@@ -1023,14 +1157,9 @@ public function getConfigurationFormProperties(array &$form, FormStateInterface
     $properties = parent::getConfigurationFormProperties($form, $form_state);
     foreach ($properties as $key => $value) {
       // Convert composite element access and required to boolean value.
-      if (strpos($key, '__access') || strpos($key, '__required')) {
+      if (preg_match('/__(access|required)$/', $key)) {
         $properties[$key] = (boolean) $value;
       }
-      // If the entire element is required remove required property for
-      // composite elements.
-      if (!empty($properties['required']) && strpos($key, '__required')) {
-        unset($properties[$key]);
-      }
     }
     return $properties;
   }
@@ -1046,9 +1175,38 @@ public function getConfigurationFormProperties(array &$form, FormStateInterface
    *   A composite element.
    */
   public function initializeCompositeElements(array &$element) {
-    // @see \Drupal\webform\Plugin\WebformElement\WebformCompositeBase::getInitializedCompositeElement
+    /** @var \Drupal\webform\Element\WebformCompositeInterface $class */
     $class = $this->getFormElementClassDefinition();
     $element['#webform_composite_elements'] = $class::initializeCompositeElements($element);
+    $this->initializeCompositeElementsRecursive($element, $element['#webform_composite_elements']);
+  }
+
+  /**
+   * Initialize a composite's elements recursively.
+   *
+   * @param array $element
+   *   A render array for the current element.
+   * @param array $composite_elements
+   *   A render array containing a composite's elements.
+   */
+  protected function initializeCompositeElementsRecursive(array &$element, array &$composite_elements) {
+    foreach ($composite_elements as $composite_key => &$composite_element) {
+      if (Element::property($composite_key)) {
+        continue;
+      }
+
+      // Set composite id, key, and parent key.
+      // @see \Drupal\webform\Entity\Webform::initElementsRecursive
+      if (isset($element['#webform_id'])) {
+        $composite_element['#webform_composite_id'] = $element['#webform_id'] . '--' . $composite_key;
+      }
+      if (isset($element['#webform_key'])) {
+        $composite_element['#webform_composite_key'] = $element['#webform_key'] . '__' . $composite_key;
+        $composite_element['#webform_composite_parent_key'] = $element['#webform_key'];
+      }
+
+      $this->initializeCompositeElementsRecursive($element, $composite_element);
+    }
   }
 
   /**
@@ -1106,6 +1264,107 @@ protected function getCompositeElementOptions($composite_key) {
     return $options;
   }
 
+  /****************************************************************************/
+  // Composite managed file methods.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(array &$element, WebformSubmissionInterface $webform_submission, $update = TRUE) {
+    $webform = $webform_submission->getWebform();
+    if ($webform->isResultsDisabled() || !$this->hasManagedFiles($element)) {
+      return;
+    }
+
+    $original_data = $webform_submission->getOriginalData();
+    $data = $webform_submission->getData();
+
+    $composite_elements_managed_files = $this->getManagedFiles($element);
+    foreach ($composite_elements_managed_files as $composite_key) {
+      $original_fids = $this->getManagedFileIdsFromData($element, $original_data, $composite_key);
+      $fids = $this->getManagedFileIdsFromData($element, $data, $composite_key);
+
+      // Delete the old file uploads.
+      $delete_fids = array_diff($original_fids, $fids);
+      WebformManagedFileBase::deleteFiles($webform_submission, $delete_fids);
+
+      // Add new files.
+      if ($fids) {
+        $composite_element = $this->getInitializedCompositeElement($element, $composite_key);
+        /** @var \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase $composite_element_plugin */
+        $composite_element_plugin = $this->elementManager->getElementInstance($composite_element);
+        $composite_element_plugin->addFiles($composite_element, $webform_submission, $fids);
+      }
+    }
+  }
+
+  /**
+   * Get composite element's file ids from data array.
+   *
+   * @param array $element
+   *   A composite element.
+   * @param array $data
+   *   A submission data array.
+   * @param string $composite_key
+   *   The composite sub-element key.
+   *
+   * @return array
+   *   An array of file ids.
+   */
+  protected function getManagedFileIdsFromData(array $element, array $data, $composite_key) {
+    $element_key = $element['#webform_key'];
+
+    if (empty($data[$element_key])) {
+      return [];
+    }
+
+    $fids = [];
+    $items = ($this->hasMultipleValues($element)) ? $data[$element_key] : [$data[$element_key]];
+    foreach ($items as $item) {
+      if (!empty($item[$composite_key])) {
+        $fids[] = $item[$composite_key];
+      }
+    }
+    return $fids;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postDelete(array &$element, WebformSubmissionInterface $webform_submission) {
+    // Uploaded files are deleted via the webform submission.
+    // This ensures that all files associated with a submission are deleted.
+    // @see \Drupal\webform\WebformSubmissionStorage::delete
+  }
+
+  /**
+   * Get composite's managed file elements.
+   *
+   * @param array $element
+   *   A composite element.
+   *
+   * @return array
+   *   An array of managed file element keys.
+   */
+  protected function getManagedFiles(array $element) {
+    if (isset($this->elementsManagedFiles)) {
+      return $this->elementsManagedFiles;
+    }
+
+    $composite_elements = WebformElementHelper::getFlattened(
+      $this->getInitializedCompositeElement($element)
+    );
+
+    foreach ($composite_elements as $composite_key => $composite_element) {
+      $composite_element_plugin = $this->elementManager->getElementInstance($composite_element);
+      if ($composite_element_plugin instanceof WebformManagedFileBase) {
+        $this->elementsManagedFiles[$composite_key] = $composite_key;
+      }
+    }
+    return $this->elementsManagedFiles;
+  }
+
   /****************************************************************************/
   // Composite helper methods.
   /****************************************************************************/
@@ -1130,9 +1389,8 @@ public static function isSupportedElementType($type) {
       || $element_plugin->isComposite()
       || $element_plugin->isContainer($element)
       || $element_plugin->hasMultipleValues($element)
-      || $element_plugin instanceof WebformElementEntityReferenceInterface
-      || $element_plugin instanceof WebformComputedBase
-      || $element_plugin instanceof WebformManagedFileBase) {
+      || ($element_plugin instanceof WebformElementEntityReferenceInterface && !($element_plugin instanceof WebformManagedFileBase))
+      || $element_plugin instanceof WebformComputedBase) {
       return FALSE;
     }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformComputedBase.php b/web/modules/webform/src/Plugin/WebformElement/WebformComputedBase.php
index 064d29e6ff2cadd7c326a88bfbbe6f0517650091..5700621f59b571d88da6c3ae706d8efa8d4e8260 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformComputedBase.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformComputedBase.php
@@ -4,14 +4,15 @@
 
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Mail\MailFormatHelper;
-use Drupal\Core\Render\Element;
+use Drupal\webform\Element\WebformComputedTwig as WebformComputedTwigElement;
 use Drupal\webform\Element\WebformComputedBase as WebformComputedBaseElement;
 use Drupal\webform\Plugin\WebformElementBase;
 use Drupal\webform\Plugin\WebformElementDisplayOnInterface;
+use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
- * Provides a base clase for 'webform_computed' elements.
+ * Provides a base class for 'webform_computed' elements.
  */
 abstract class WebformComputedBase extends WebformElementBase implements WebformElementDisplayOnInterface {
 
@@ -28,6 +29,7 @@ public function getDefaultProperties() {
       'display_on' => static::DISPLAY_ON_BOTH,
       // Description/Help.
       'help' => '',
+      'help_title' => '',
       'description' => '',
       'more' => '',
       'more_title' => '',
@@ -35,11 +37,14 @@ public function getDefaultProperties() {
       'title_display' => '',
       'description_display' => '',
       // Computed values.
-      'value' => '',
+      'template' => '',
       'mode' => WebformComputedBaseElement::MODE_AUTO,
+      'hide_empty' => FALSE,
       'store' => FALSE,
+      'ajax' => FALSE,
       // Attributes.
       'wrapper_attributes' => [],
+      'label_attributes' => [],
     ] + $this->getDefaultBaseProperties();
   }
 
@@ -68,19 +73,14 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
     if (!$this->isDisplayOn($element, static::DISPLAY_ON_FORM)) {
       $element['#access'] = FALSE;
     }
-
-    $element['#element_validate'][] = [get_class($this), 'validateWebformComputed'];
   }
 
   /**
    * {@inheritdoc}
    */
-  public function replaceTokens(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
-    foreach ($element as $key => $value) {
-      if (!Element::child($key) && !in_array($key, ['#value'])) {
-        $element[$key] = $this->tokenManager->replace($value, $webform_submission);
-      }
-    }
+  protected function prepareElementValidateCallbacks(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    parent::prepareElementValidateCallbacks($element, $webform_submission);
+    $element['#element_validate'][] = [get_class($this), 'validateWebformComputed'];
   }
 
   /**
@@ -145,7 +145,7 @@ public function preview() {
     return [
       '#type' => $this->getTypeName(),
       '#title' => $this->getPluginLabel(),
-      '#value' => $this->t('This is a @label value.', ['@label' => $this->getPluginLabel()]),
+      '#template' => $this->t('This is a @label value.', ['@label' => $this->getPluginLabel()]),
     ];
   }
 
@@ -155,9 +155,6 @@ public function preview() {
   public function form(array $form, FormStateInterface $form_state) {
     $form = parent::form($form, $form_state);
 
-    // Remove value element so that it appears under computed fieldset.
-    unset($form['element']['value']);
-
     $form['computed'] = [
       '#type' => 'fieldset',
       '#title' => $this->t('Computed settings'),
@@ -185,18 +182,39 @@ public function form(array $form, FormStateInterface $form_state) {
         WebformComputedBaseElement::MODE_TEXT => t('Plain text'),
       ],
     ];
-    $form['computed']['value'] = [
+    $form['computed']['template'] = [
       '#type' => 'webform_codemirror',
       '#mode' => 'text',
       '#title' => $this->t('Computed value/markup'),
     ];
+    $form['computed']['whitespace'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Remove whitespace around the'),
+      '#empty_option' => $this->t('- None -'),
+      '#options' => [
+        WebformComputedTwigElement::WHITESPACE_TRIM => $this->t('computed value'),
+        WebformComputedTwigElement::WHITESPACE_SPACELESS => $this->t('computed value and between HTML tags'),
+      ],
+    ];
+    $form['computed']['hide_empty'] = [
+      '#type' => 'checkbox',
+      '#return_value' => TRUE,
+      '#title' => $this->t('Hide empty'),
+      '#description' => $this->t('If checked the computed elements will be hidden from display when the value is an empty string.'),
+    ];
     $form['computed']['store'] = [
       '#type' => 'checkbox',
       '#return_value' => TRUE,
       '#title' => $this->t('Store value in the database'),
       '#description' => $this->t('The value will be stored in the database. As a result, it will only be recalculated when the submission is updated. This option is required when accessing the computed element through Views.'),
     ];
-    $form['computed']['tokens'] = ['#access' => TRUE, '#weight' => 10] + $this->tokenManager->buildTreeLink();
+    $form['computed']['ajax'] = [
+      '#type' => 'checkbox',
+      '#return_value' => TRUE,
+      '#title' => $this->t('Automatically update the computed value using Ajax'),
+      '#description' => $this->t('If checked, any element used within the computed value/markup will trigger any automatic update.'),
+    ];
+    $form['computed']['tokens'] = ['#access' => TRUE, '#weight' => 10] + $this->tokenManager->buildTreeElement();
     return $form;
   }
 
@@ -219,7 +237,7 @@ public function preSave(array &$element, WebformSubmissionInterface $webform_sub
     $key = $element['#webform_key'];
     $data = $webform_submission->getData();
     if (!empty($element['#store'])) {
-      $data[$key] = $this->processValue($element, $webform_submission);
+      $data[$key] = (string) $this->processValue($element, $webform_submission);
     }
     else {
       // Always unset the value.
@@ -228,6 +246,42 @@ public function preSave(array &$element, WebformSubmissionInterface $webform_sub
     $webform_submission->setData($data);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(array &$element, WebformSubmissionInterface $webform_submission, $update = TRUE) {
+    if ($update || empty($element['#store']) || $webform_submission->getWebform()->getSetting('results_disabled')) {
+      return;
+    }
+
+    // Recalculate the stored computed value to account new a submission's
+    // generated sid and serial.
+    $key = $element['#webform_key'];
+    $value = (string) $this->processValue($element, $webform_submission);
+
+    // Update the submission's value.
+    $webform_submission->setElementData($key, $value);
+
+    // The below database update is a one-off solution because there is
+    // currently no other instances where a single element's value
+    // needs to be updated.
+    // @see \Drupal\webform\WebformSubmissionStorage::saveData
+    $fields = [
+      'webform_id' => $webform_submission->getWebform()->id(),
+      'sid' => $webform_submission->id(),
+      'name' => $key,
+      'property' => '',
+      'delta' => 0,
+      'value' => $value,
+    ];
+    \Drupal::database()->update('webform_submission_data')
+      ->fields($fields)
+      ->condition('webform_id', $fields['webform_id'])
+      ->condition('sid', $fields['sid'])
+      ->condition('name', $fields['name'])
+      ->execute();
+  }
+
   /**
    * Get computed element markup.
    *
@@ -257,4 +311,19 @@ protected function processValue(array $element, WebformSubmissionInterface $webf
     return $class::processValue($element, $webform_submission);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getTestValues(array $element, WebformInterface $webform, array $options = []) {
+    // Computed elements should never get a test value.
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorInputValue($selector, $trigger, array $element, WebformSubmissionInterface $webform_submission) {
+    return (string) $this->processValue($element, $webform_submission);
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformComputedTwig.php b/web/modules/webform/src/Plugin/WebformElement/WebformComputedTwig.php
index bcb5934d7af7fa2280e134c6227ab471af92ff11..a1aa8caa9edef4aedd4c4c7ecc49197d95b392e0 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformComputedTwig.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformComputedTwig.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\webform\Plugin\WebformElement;
 
-use Drupal\Component\Utility\Html;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\Twig\TwigExtension;
@@ -19,6 +18,15 @@
  */
 class WebformComputedTwig extends WebformComputedBase {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultProperties() {
+    return [
+      'whitespace' => '',
+    ] + parent::getDefaultProperties();
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -26,7 +34,7 @@ public function form(array $form, FormStateInterface $form_state) {
     $form = parent::form($form, $form_state);
 
     $form['computed']['help'] = TwigExtension::buildTwigHelp();
-    $form['computed']['value']['#mode'] = 'twig';
+    $form['computed']['template']['#mode'] = 'twig';
 
     // Set #access so that help is always visible.
     WebformElementHelper::setPropertyRecursive($form['computed']['help'], '#access', TRUE);
@@ -34,27 +42,4 @@ public function form(array $form, FormStateInterface $form_state) {
     return $form;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
-    parent::validateConfigurationForm($form, $form_state);
-
-    // Validate Twig markup with no context.
-    try {
-      $build = [
-        '#type' => 'inline_template',
-        '#template' => $form_state->getValue('value'),
-        '#context' => [],
-      ];
-      \Drupal::service('renderer')->renderPlain($build);
-    }
-    catch (\Exception $exception) {
-      $form_state->setErrorByName('markup', [
-        'message' => ['#markup' => $this->t('Failed to render computed Twig value due to error.'), '#suffix' => '<br /><br />'],
-        'error' => ['#markup' => Html::escape($exception->getMessage()), '#prefix' => '<pre>', '#suffix' => '</pre>'],
-      ]);
-    }
-  }
-
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformCustomComposite.php b/web/modules/webform/src/Plugin/WebformElement/WebformCustomComposite.php
index 50b64e4ffa6ddfacafdea0dbc50c902a41f5431f..35ef76a1ef8b78edf0bda309617a87ffaf173c42 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformCustomComposite.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformCustomComposite.php
@@ -3,7 +3,6 @@
 namespace Drupal\webform\Plugin\WebformElement;
 
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
@@ -120,7 +119,7 @@ protected function buildCompositeElementsTable() {
     return [
       '#type' => 'webform_element_composite',
       '#title' => $this->t('Elements'),
-      '#title_display' => $this->t('Invisible'),
+      '#title_display' => 'invisible',
     ];
   }
 
@@ -154,36 +153,6 @@ public function preview() {
     ];
   }
 
-  /****************************************************************************/
-  // Test methods.
-  /****************************************************************************/
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getTestValues(array $element, WebformInterface $webform, array $options = []) {
-    /** @var \Drupal\webform\WebformSubmissionGenerateInterface $generate */
-    $generate = \Drupal::service('webform_submission.generate');
-
-    $composite_elements = $element['#element'];
-
-    // Initialize, prepare, and populate composite sub-element.
-    foreach ($composite_elements as $composite_key => $composite_element) {
-      $this->elementManager->initializeElement($composite_element);
-      $composite_elements[$composite_key] = $composite_element;
-    }
-
-    $values = [];
-    for ($i = 1; $i <= 3; $i++) {
-      $value = [];
-      foreach ($composite_elements as $composite_key => $composite_element) {
-        $value[$composite_key] = $generate->getTestValue($webform, $composite_key, $composite_element, $options);
-      }
-      $values[] = $value;
-    }
-    return $values;
-  }
-
   /****************************************************************************/
   // Composite element methods.
   /****************************************************************************/
@@ -194,10 +163,10 @@ public function getTestValues(array $element, WebformInterface $webform, array $
   public function initializeCompositeElements(array &$element) {
     $element['#webform_composite_elements'] = [];
     foreach ($element['#element'] as $composite_key => $composite_element) {
-      // Initialize composite sub-element.
       $this->elementManager->initializeElement($composite_element);
       $element['#webform_composite_elements'][$composite_key] = $composite_element;
     }
+    $this->initializeCompositeElementsRecursive($element, $element['#webform_composite_elements']);
   }
 
   /**
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformElement.php b/web/modules/webform/src/Plugin/WebformElement/WebformElement.php
index e5181722aecb4f27fa9e7206f53d604cdcd2273f..bde9844d486b02d56578f426909453bfba6cf1a3 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformElement.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformElement.php
@@ -4,7 +4,6 @@
 
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\webform\Plugin\WebformElementBase;
-use Drupal\Core\Url as UrlGenerator;
 
 /**
  * Provides a 'generic' element. Used as a fallback.
@@ -55,18 +54,18 @@ public function form(array $form, FormStateInterface $form_state) {
    */
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
     $form = parent::buildConfigurationForm($form, $form_state);
+    $form['custom']['#type'] = 'fieldset';
     $form['custom']['#title'] = $this->t('Element settings');
+    $form['custom']['#weight'] = 100;
     $form['custom']['custom']['#title'] = $this->t('Properties');
+    return $form;
+  }
 
-    // Add link to theme API documentation.
-    $theme = (isset($this->configuration['#theme'])) ? $this->configuration['#theme'] : '';
-    if (function_exists('template_preprocess_' . $theme)) {
-      $t_args = [
-        ':href' => UrlGenerator::fromUri('https://api.drupal.org/api/drupal/core!includes!theme.inc/function/template_preprocess_' . $theme)->toString(),
-        '%label' => $theme,
-      ];
-      $form['custom']['#description'] = $this->t('Read the the %label template\'s <a href=":href">API documentation</a>.', $t_args);
-    }
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildConfigurationFormTabs(array $form, FormStateInterface $form_state) {
+    // Generic elements do not need use tabs.
     return $form;
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformEmailConfirm.php b/web/modules/webform/src/Plugin/WebformElement/WebformEmailConfirm.php
index c91d8ba5169744c1ba8592d3248e755f94d3e47d..d8deaf2581bda5148cf1a83c85522f610e536ea8 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformEmailConfirm.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformEmailConfirm.php
@@ -28,6 +28,8 @@ public function getDefaultProperties() {
       'confirm__title' => '',
       'confirm__description' => '',
       'confirm__placeholder' => '',
+      // Wrapper.
+      'wrapper_type' => 'fieldset',
     ];
     unset(
       $properties['multiple'],
@@ -68,6 +70,13 @@ public function form(array $form, FormStateInterface $form_state) {
       '#type' => 'textfield',
       '#title' => $this->t('Email confirm placeholder'),
     ];
+
+    // Remove unsupported title and description display from composite elements.
+    if ($this->isComposite()) {
+      unset($form['form']['display_container']['title_display']['#options']['inline']);
+      unset($form['form']['display_container']['description_display']['#options']['tooltip']);
+    }
+
     return $form;
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformEntityOptionsTrait.php b/web/modules/webform/src/Plugin/WebformElement/WebformEntityOptionsTrait.php
index 13adfa36e815c18847ff884527c901c42c2dca75..987244746d93ffc7e5c4c8e2590232c928bbe083 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformEntityOptionsTrait.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformEntityOptionsTrait.php
@@ -39,4 +39,12 @@ protected function getElementSelectorInputsOptions(array $element) {
     return parent::getElementSelectorInputsOptions($element);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorSourceValues(array $element) {
+    $this->setOptions($element);
+    return parent::getElementSelectorSourceValues($element);
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformEntityReferenceTrait.php b/web/modules/webform/src/Plugin/WebformElement/WebformEntityReferenceTrait.php
index ad2a998c63076b0f2535d07213eec3a637c8b18b..a54de5d5c502f743bb7d8329def6d0229cbed133 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformEntityReferenceTrait.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformEntityReferenceTrait.php
@@ -6,6 +6,7 @@
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
+use Drupal\Core\Url as UrlGenerator;
 use Drupal\webform\Element\WebformEntityTrait;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformSubmissionInterface;
@@ -40,7 +41,8 @@ public function getRelatedTypes(array $element) {
    */
   protected function formatHtmlItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
     $entity = $this->getTargetEntity($element, $webform_submission, $options);
-    if (!$entity) {
+
+    if (empty($entity)) {
       return '';
     }
 
@@ -55,11 +57,32 @@ protected function formatHtmlItem(array $element, WebformSubmissionInterface $we
         return $this->formatTextItem($element, $webform_submission, $options);
 
       case 'link':
-        return [
-          '#type' => 'link',
-          '#title' => $entity->label(),
-          '#url' => $entity->toUrl()->setAbsolute(TRUE),
-        ];
+        if ($entity->hasLinkTemplate('canonical')) {
+          return [
+            '#type' => 'link',
+            '#title' => $entity->label(),
+            '#url' => $entity->toUrl()->setAbsolute(TRUE),
+          ];
+        }
+        else {
+          switch ($entity->getEntityTypeId()) {
+            case 'file':
+              /** @var \Drupal\file\FileInterface $entity */
+              if ($entity->access('download')) {
+                return [
+                  '#type' => 'link',
+                  '#title' => $entity->label(),
+                  '#url' => UrlGenerator::fromUri(file_create_url($entity->getFileUri())),
+                ];
+              }
+              else {
+                return $this->formatTextItem($element, $webform_submission, $options);
+              }
+
+            default:
+              return $this->formatTextItem($element, $webform_submission, $options);
+          }
+        }
 
       default:
         return \Drupal::entityTypeManager()->getViewBuilder($entity->getEntityTypeId())->view($entity, $format);
@@ -112,10 +135,10 @@ protected function formatTextItem(array $element, WebformSubmissionInterface $we
    * {@inheritdoc}
    */
   public function getTestValues(array $element, WebformInterface $webform, array $options = []) {
-    $this->setOptions($element);
-    $target_type = $this->getTargetType($element);
+    $this->setOptions($element, ['limit' => 10, 'random' => TRUE]);
     // Exclude 'anonymous' user.
-    if ($target_type == 'user') {
+    $target_type = $this->getTargetType($element);
+    if ($target_type === 'user') {
       unset($element['#options'][0]);
     }
     return array_keys($element['#options']);
@@ -322,9 +345,11 @@ public function buildExportRecord(array $element, WebformSubmissionInterface $we
    *
    * @param array $element
    *   An element.
+   * @param array $settings
+   *   An array of settings used to limit and randomize options.
    */
-  protected function setOptions(array &$element) {
-    WebformEntityTrait::setOptions($element);
+  protected function setOptions(array &$element, array $settings = []) {
+    WebformEntityTrait::setOptions($element, $settings);
   }
 
   /**
@@ -535,16 +560,16 @@ public function form(array $form, FormStateInterface $form_state) {
       '#submit' => [[get_called_class(), 'submitEntityReferenceCallback']],
       // Refresh the entity reference details container.
       '#ajax' => [
-        'callback' => [get_called_class(), 'entityReferenceAjaxCallback'],
+        'callback' => [get_called_class(), 'ajaxEntityReferenceCallback'],
         'wrapper' => 'webform-entity-reference-selection-wrapper',
         'progress' => ['type' => 'fullscreen'],
       ],
-      // Hide button, add submit button trigger class, and disable validation.
+      // Disable validation, hide button, add submit button trigger class.
       '#attributes' => [
+        'formnovalidate' => 'formnovalidate',
         'class' => [
           'js-hide',
           'js-webform-entity-reference-submit',
-          'js-webform-novalidate',
         ],
       ],
     ];
@@ -657,7 +682,7 @@ public static function submitEntityReferenceCallback(array $form, FormStateInter
    * @return array
    *   The properties element.
    */
-  public static function entityReferenceAjaxCallback(array $form, FormStateInterface $form_state) {
+  public static function ajaxEntityReferenceCallback(array $form, FormStateInterface $form_state) {
     $button = $form_state->getTriggeringElement();
     $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
     return $element;
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformHorizontalRule.php b/web/modules/webform/src/Plugin/WebformElement/WebformHorizontalRule.php
index 82b7217f39d9ebe9fb713637a58238f018b81447..cbf50a9f1160d3dac057bd6de25a9471db36714f 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformHorizontalRule.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformHorizontalRule.php
@@ -82,7 +82,7 @@ public function buildText(array $element, WebformSubmissionInterface $webform_su
       return [];
     }
 
-    return PHP_EOL . '---' . PHP_EOL;
+    return ['#plain_text' => PHP_EOL . '---' . PHP_EOL];
   }
 
   /**
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformImageFile.php b/web/modules/webform/src/Plugin/WebformElement/WebformImageFile.php
index 3c715e19722ca72682b0b77f7f62c7a94af6398a..bb03aead0a82751c3b706bf777a8057d12a72ff2 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformImageFile.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformImageFile.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\webform\Plugin\WebformElement;
 
+use Drupal\Core\Form\FormStateInterface;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
@@ -20,6 +21,30 @@
  */
 class WebformImageFile extends WebformManagedFileBase {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultProperties() {
+    return parent::getDefaultProperties() + [
+      'max_resolution' => '',
+      'min_resolution' => '',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    parent::prepare($element, $webform_submission);
+
+    // Add upload resolution validation.
+    $max_resolution = $this->getElementProperty($element, 'max_resolution');
+    $min_resolution = $this->getElementProperty($element, 'min_resolution');
+    if ($max_resolution || $min_resolution) {
+      $element['#upload_validators']['file_validate_image_resolution'] = [$max_resolution, $min_resolution];
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -39,7 +64,7 @@ public function getItemFormats() {
     // Add support :image, :link, and :modal.
     $label = (string) $this->t('Original Image');
     $t_args = ['@label' => $label];
-    $formats[$label][":image"] = $this->t('@label: Image', $t_args);;
+    $formats[$label][":image"] = $this->t('@label: Image', $t_args);
     $formats[$label][":link"] = $this->t('@label: Link', $t_args);
     $formats[$label][":modal"] = $this->t('@label: Modal', $t_args);
     if (\Drupal::moduleHandler()->moduleExists('image')) {
@@ -47,7 +72,7 @@ public function getItemFormats() {
       foreach ($image_styles as $id => $image_style) {
         $label = (string) $image_style->label();
         $t_args = ['@label' => $label];
-        $formats[$label]["$id:image"] = $this->t('@label: Image', $t_args);;
+        $formats[$label]["$id:image"] = $this->t('@label: Image', $t_args);
         $formats[$label]["$id:link"] = $this->t('@label: Link', $t_args);
         $formats[$label]["$id:modal"] = $this->t('@label: Modal', $t_args);
       }
@@ -83,4 +108,35 @@ protected function formatHtmlItem(array $element, WebformSubmissionInterface $we
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+    $form['image'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Image settings'),
+    ];
+    $form['image'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Image settings'),
+    ];
+    $form['image']['max_resolution'] = [
+      '#type' => 'webform_image_resolution',
+      '#title' => t('Maximum image resolution'),
+      '#description' => t('The maximum allowed image size expressed as WIDTH×HEIGHT (e.g. 640×480). Leave blank for no restriction. If a larger image is uploaded, it will be resized to reflect the given width and height. Resizing images on upload will cause the loss of <a href="http://wikipedia.org/wiki/Exchangeable_image_file_format">EXIF data</a> in the image.'),
+      '#width_title' => t('Maximum width'),
+      '#height_title' => t('Maximum height'),
+    ];
+    $form['image']['min_resolution'] = [
+      '#type' => 'webform_image_resolution',
+      '#title' => t('Minimum image resolution'),
+      '#description' => t('The minimum allowed image size expressed as WIDTH×HEIGHT (e.g. 640×480). Leave blank for no restriction. If a smaller image is uploaded, it will be rejected.'),
+      '#width_title' => t('Minimum width'),
+      '#height_title' => t('Minimum height'),
+    ];
+
+    return $form;
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformLikert.php b/web/modules/webform/src/Plugin/WebformElement/WebformLikert.php
index 62d70df42a1b4a3bb6f9e6a76d8301a57bb25b60..4ac156a2200830793066ffb01b34fab9e0a8896b 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformLikert.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformLikert.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\webform\Element\WebformLikert as WebformLikertElement;
 use Drupal\webform\Entity\WebformOptions;
+use Drupal\webform\Utility\WebformAccessibilityHelper;
 use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\Utility\WebformOptionsHelper;
 use Drupal\webform\Plugin\WebformElementBase;
@@ -34,6 +35,7 @@ public function getDefaultProperties() {
       'default_value' => [],
       // Description/Help.
       'help' => '',
+      'help_title' => '',
       'description' => '',
       'more' => '',
       'more_title' => '',
@@ -48,7 +50,9 @@ public function getDefaultProperties() {
       'format' => $this->getItemDefaultFormat(),
       'format_html' => '',
       'format_text' => '',
+      'format_attributes' => [],
       // Likert settings.
+      'sticky' => TRUE,
       'questions' => [],
       'questions_description_display' => 'description',
       'questions_randomize' => FALSE,
@@ -59,6 +63,7 @@ public function getDefaultProperties() {
       'na_answer_text' => $this->t('N/A'),
       // Attributes.
       'wrapper_attributes' => [],
+      'label_attributes' => [],
       // iCheck settings.
       'icheck' => '',
     ] + $this->getDefaultBaseProperties();
@@ -110,7 +115,7 @@ protected function formatHtmlItem(array $element, WebformSubmissionInterface $we
         // HTML emails.
         $header = [];
         $header['likert_question'] = [
-          'data' => '',
+          'data' => WebformAccessibilityHelper::buildVisuallyHidden(t('Questions')),
           'align' => 'left',
           'width' => '40%',
         ];
@@ -149,6 +154,7 @@ protected function formatHtmlItem(array $element, WebformSubmissionInterface $we
           '#type' => 'table',
           '#header' => $header,
           '#rows' => $rows,
+          '#sticky' => $this->getElementProperty($element, 'sticky'),
           '#attributes' => [
             'class' => ['webform-likert-table'],
           ],
@@ -380,6 +386,20 @@ protected function getElementSelectorInputsOptions(array $element) {
     return $selectors;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorSourceValues(array $element) {
+    $selector_options = $this->getElementSelectorOptions($element);
+    $selectors = reset($selector_options);
+
+    $source_values = [];
+    foreach (array_keys($selectors) as $selector) {
+      $source_values[$selector] = $element['#answers'];
+    }
+    return $source_values;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -446,7 +466,8 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['likert']['na_answer_text'] = [
       '#type' => 'textfield',
       '#title' => $this->t('N/A answer text'),
-      '#description' => $this->t('Text display display on webform.'),
+      '#description' => $this->t('Text displayed on the webform.'),
+      '#attributes' => ['data-webform-states-no-clear' => TRUE],
       '#states' => [
         'visible' => [
           ':input[name="properties[na_answer]"]' => ['checked' => TRUE],
@@ -456,6 +477,12 @@ public function form(array $form, FormStateInterface $form_state) {
         ],
       ],
     ];
+    $form['likert']['sticky'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable Drupal style "sticky" table headers (Javascript)'),
+      '#description' => $this->t('If checked, the answers will float at the top of the page as the user scrolls-thru the questions.'),
+      '#return_value' => TRUE,
+    ];
     return $form;
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformLink.php b/web/modules/webform/src/Plugin/WebformElement/WebformLink.php
index 55c2b6e9c64f3fe115de3ffceea4e05039613fdf..cec62f56c567835654955d6436226c40d699b23d 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformLink.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformLink.php
@@ -19,6 +19,21 @@
  */
 class WebformLink extends WebformCompositeBase {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultProperties() {
+    $properties = parent::getDefaultProperties();
+
+    // Link does not have select menus.
+    unset(
+      $properties['select2'],
+      $properties['chosed']
+    );
+
+    return $properties;
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformLocationBase.php b/web/modules/webform/src/Plugin/WebformElement/WebformLocationBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..d3973245166382fb1c2b1c1e87cd5da8eed10a87
--- /dev/null
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformLocationBase.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Drupal\webform\Plugin\WebformElement;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a base 'location' element.
+ */
+abstract class WebformLocationBase extends WebformCompositeBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultProperties() {
+    $properties = [
+      'title' => '',
+      'default_value' => [],
+      'multiple' => FALSE,
+      // Description/Help.
+      'help' => '',
+      'help_title' => '',
+      'description' => '',
+      'more' => '',
+      'more_title' => '',
+      // Form display.
+      'title_display' => '',
+      'description_display' => '',
+      'disabled' => FALSE,
+      // Form validation.
+      'required' => FALSE,
+      'required_error' => '',
+      // Attributes.
+      'wrapper_attributes' => [],
+      'label_attributes' => [],
+      // Submission display.
+      'format' => $this->getItemDefaultFormat(),
+      'format_html' => '',
+      'format_text' => '',
+      'format_items' => $this->getItemsDefaultFormat(),
+      'format_items_html' => '',
+      'format_items_text' => '',
+    ] + $this->getDefaultBaseProperties();
+
+    $composite_elements = $this->getCompositeElements();
+    foreach ($composite_elements as $composite_key => $composite_element) {
+      $properties[$composite_key . '__title'] = (string) $composite_element['#title'];
+      // The value is always visible and supports a custom placeholder.
+      if ($composite_key == 'value') {
+        $properties[$composite_key . '__placeholder'] = '';
+      }
+      else {
+        $properties[$composite_key . '__access'] = FALSE;
+      }
+    }
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function initialize(array &$element) {
+    // Hide all composite elements by default.
+    $composite_elements = $this->getCompositeElements();
+    foreach ($composite_elements as $composite_key => $composite_element) {
+      if ($composite_key != 'value' && !isset($element['#' . $composite_key . '__access'])) {
+        $element['#' . $composite_key . '__access'] = FALSE;
+      }
+    }
+
+    parent::initialize($element);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+
+    // Reverted #required label.
+    $form['validation']['required']['#description'] = $this->t('Check this option if the user must enter a value.');
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildCompositeElementsTable() {
+    $header = [
+      $this->t('Key'),
+      $this->t('Title/Placeholder'),
+      $this->t('Visible'),
+    ];
+
+    $rows = [];
+    $composite_elements = $this->getCompositeElements();
+    foreach ($composite_elements as $composite_key => $composite_element) {
+      $title = (isset($composite_element['#title'])) ? $composite_element['#title'] : $composite_key;
+      $type = isset($composite_element['#type']) ? $composite_element['#type'] : NULL;
+      $t_args = ['@title' => $title];
+      $attributes = ['style' => 'width: 100%; margin-bottom: 5px'];
+
+      $row = [];
+
+      // Key.
+      $row[$composite_key . '__key'] = [
+        '#markup' => $composite_key,
+        '#access' => TRUE,
+      ];
+
+      // Title, placeholder, and description.
+      if ($type) {
+        $row['title_and_description'] = [
+          'data' => [
+            $composite_key . '__title' => [
+              '#type' => 'textfield',
+              '#title' => $this->t('@title title', $t_args),
+              '#title_display' => 'invisible',
+              '#placeholder' => $this->t('Enter title…'),
+              '#attributes' => $attributes,
+            ],
+            $composite_key . '__placeholder' => [
+              '#type' => 'textfield',
+              '#title' => $this->t('@title placeholder', $t_args),
+              '#title_display' => 'invisible',
+              '#placeholder' => $this->t('Enter placeholder…'),
+              '#attributes' => $attributes,
+            ],
+          ],
+        ];
+      }
+      else {
+        $row['title_and_description'] = ['data' => ['']];
+      }
+
+      // Access.
+      if ($composite_key === 'value') {
+        $row[$composite_key . '__access'] = [
+          '#type' => 'checkbox',
+          '#default_value' => TRUE,
+          '#disabled' => TRUE,
+          '#access' => TRUE,
+        ];
+      }
+      else {
+        $row[$composite_key . '__access'] = [
+          '#type' => 'checkbox',
+          '#return_value' => TRUE,
+        ];
+      }
+
+      $rows[$composite_key] = $row;
+    }
+
+    return [
+      '#type' => 'table',
+      '#header' => $header,
+    ] + $rows;
+  }
+
+}
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformLocation.php b/web/modules/webform/src/Plugin/WebformElement/WebformLocationGeocomplete.php
similarity index 55%
rename from web/modules/webform/src/Plugin/WebformElement/WebformLocation.php
rename to web/modules/webform/src/Plugin/WebformElement/WebformLocationGeocomplete.php
index c54850863410341d9217f2206e0e55a67d3d2284..0444f113f8c37d52e65e1b4fa2d64d8841cba227 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformLocation.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformLocationGeocomplete.php
@@ -8,92 +8,39 @@
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
- * Provides an 'location' element.
+ * Provides an 'location' element using Geocomplete.
  *
  * @WebformElement(
- *   id = "webform_location",
- *   label = @Translation("Location"),
- *   description = @Translation("Provides a form element to collect valid location information (address, longitude, latitude, geolocation) using Google's location auto completion API."),
+ *   id = "webform_location_geocomplete",
+ *   label = @Translation("Location (Geocomplete)"),
+ *   description = @Translation("Provides a form element to collect valid location information (address, longitude, latitude, geolocation) using Google Maps API's Geocoding and Places Autocomplete."),
  *   category = @Translation("Composite elements"),
  *   multiline = TRUE,
  *   composite = TRUE,
  *   states_wrapper = TRUE,
+ *   deprecated = TRUE,
+ *   deprecated_message = @Translation("The jQuery: Geocoding and Places Autocomplete Plugin library is not being maintained. It has been <a href=""https://www.drupal.org/node/2991275"">deprecated</a> and will be removed before Webform 8.x-5.0."),
  * )
  */
-class WebformLocation extends WebformCompositeBase {
+class WebformLocationGeocomplete extends WebformLocationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginLabel() {
+    return $this->elementManager->isExcluded('webform_location_places') ? $this->t('Location') : parent::getPluginLabel();
+  }
 
   /**
    * {@inheritdoc}
    */
   public function getDefaultProperties() {
-    $properties = [
-      'title' => '',
-      'default_value' => [],
-      'multiple' => FALSE,
-      // Description/Help.
-      'help' => '',
-      'description' => '',
-      'more' => '',
-      'more_title' => '',
-      // Form display.
-      'title_display' => '',
-      'description_display' => '',
-      'disabled' => FALSE,
-      // Form validation.
-      'required' => FALSE,
-      'required_error' => '',
-      // Attributes.
-      'wrapper_attributes' => [],
-      // Location settings.
+    return parent::getDefaultProperties() + [
       'geolocation' => FALSE,
       'hidden' => FALSE,
       'map' => FALSE,
       'api_key' => '',
-      // Submission display.
-      'format' => $this->getItemDefaultFormat(),
-      'format_html' => '',
-      'format_text' => '',
-      'format_items' => $this->getItemsDefaultFormat(),
-      'format_items_html' => '',
-      'format_items_text' => '',
     ] + $this->getDefaultBaseProperties();
-
-    $composite_elements = $this->getCompositeElements();
-    foreach ($composite_elements as $composite_key => $composite_element) {
-      $properties[$composite_key . '__title'] = (string) $composite_element['#title'];
-      // The value is always visible and supports a custom placeholder.
-      if ($composite_key == 'value') {
-        $properties[$composite_key . '__placeholder'] = '';
-      }
-      else {
-        $properties[$composite_key . '__access'] = FALSE;
-      }
-    }
-    return $properties;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function initialize(array &$element) {
-    // Hide all composite elements by default.
-    $composite_elements = $this->getCompositeElements();
-    foreach ($composite_elements as $composite_key => $composite_element) {
-      if ($composite_key != 'value' && !isset($element['#' . $composite_key . '__access'])) {
-        $element['#' . $composite_key . '__access'] = FALSE;
-      }
-    }
-
-    parent::initialize($element);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getItemFormats() {
-    return parent::getItemFormats() + [
-      'map' => $this->t('Map'),
-    ];
   }
 
   /**
@@ -146,11 +93,9 @@ protected function formatHtmlItem(array $element, WebformSubmissionInterface $we
   /**
    * {@inheritdoc}
    */
-  public function preview() {
-    return parent::preview() + [
-      '#map' => TRUE,
-      '#geolocation' => TRUE,
-      '#format' => 'map',
+  public function getItemFormats() {
+    return parent::getItemFormats() + [
+      'map' => $this->t('Map'),
     ];
   }
 
@@ -160,9 +105,6 @@ public function preview() {
   public function form(array $form, FormStateInterface $form_state) {
     $form = parent::form($form, $form_state);
 
-    // Reverted #required label.
-    $form['validation']['required']['#description'] = $this->t('Check this option if the user must enter a value.');
-
     $form['composite']['geolocation'] = [
       '#type' => 'checkbox',
       '#title' => $this->t("Use the browser's Geolocation as the default value"),
@@ -216,86 +158,23 @@ public function form(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  protected function buildCompositeElementsTable() {
-    $header = [
-      $this->t('Key'),
-      $this->t('Title/Placeholder'),
-      $this->t('Visible'),
+  public function preview() {
+    return parent::preview() + [
+      '#map' => TRUE,
+      '#geolocation' => TRUE,
+      '#format' => 'map',
     ];
-
-    $rows = [];
-    $composite_elements = $this->getCompositeElements();
-    foreach ($composite_elements as $composite_key => $composite_element) {
-      $title = (isset($composite_element['#title'])) ? $composite_element['#title'] : $composite_key;
-      $type = isset($composite_element['#type']) ? $composite_element['#type'] : NULL;
-      $t_args = ['@title' => $title];
-      $attributes = ['style' => 'width: 100%; margin-bottom: 5px'];
-
-      $row = [];
-
-      // Key.
-      $row[$composite_key . '__key'] = [
-        '#markup' => $composite_key,
-        '#access' => TRUE,
-      ];
-
-      // Title, placeholder, and description.
-      if ($type) {
-        $row['title_and_description'] = [
-          'data' => [
-            $composite_key . '__title' => [
-              '#type' => 'textfield',
-              '#title' => $this->t('@title title', $t_args),
-              '#title_display' => 'invisible',
-              '#placeholder' => $this->t('Enter title...'),
-              '#attributes' => $attributes,
-            ],
-            $composite_key . '__placeholder' => [
-              '#type' => 'textfield',
-              '#title' => $this->t('@title placeholder', $t_args),
-              '#title_display' => 'invisible',
-              '#placeholder' => $this->t('Enter placeholder...'),
-              '#attributes' => $attributes,
-            ],
-          ],
-        ];
-      }
-      else {
-        $row['title_and_description'] = ['data' => ['']];
-      }
-
-      // Access.
-      if ($composite_key === 'value') {
-        $row[$composite_key . '__access'] = [
-          '#type' => 'checkbox',
-          '#default_value' => TRUE,
-          '#disabled' => TRUE,
-          '#access' => TRUE,
-        ];
-      }
-      else {
-        $row[$composite_key . '__access'] = [
-          '#type' => 'checkbox',
-          '#return_value' => TRUE,
-        ];
-      }
-
-      $rows[$composite_key] = $row;
-    }
-
-    return [
-      '#type' => 'table',
-      '#header' => $header,
-    ] + $rows;
   }
 
   /**
    * {@inheritdoc}
    */
   public function getTestValues(array $element, WebformInterface $webform, array $options = []) {
-    // Use test values included in settings and not from
-    // WebformCompositeBase::getTestValues.
-    return FALSE;
+    return [
+      ['value' => 'The White House, 1600 Pennsylvania Ave NW, Washington, DC 20500, USA'],
+      ['value' => 'London SW1A 1AA, United Kingdom'],
+      ['value' => 'Moscow, Russia, 10307'],
+    ];
   }
 
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformLocationPlaces.php b/web/modules/webform/src/Plugin/WebformElement/WebformLocationPlaces.php
new file mode 100644
index 0000000000000000000000000000000000000000..30a7790a672d3c31e79d3235fc482d078de1d08e
--- /dev/null
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformLocationPlaces.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\webform\Plugin\WebformElement;
+
+use Drupal\webform\WebformInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url as UrlGenerator;
+
+/**
+ * Provides an 'location' element using Algolia Places.
+ *
+ * @WebformElement(
+ *   id = "webform_location_places",
+ *   label = @Translation("Location (Algolia Places)"),
+ *   description = @Translation("Provides a form element to collect valid location information (address, longitude, latitude, geolocation) using Algolia Places."),
+ *   category = @Translation("Composite elements"),
+ *   multiline = TRUE,
+ *   composite = TRUE,
+ *   states_wrapper = TRUE,
+ * )
+ */
+class WebformLocationPlaces extends WebformLocationBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultProperties() {
+    return [
+      'app_id' => '',
+      'api_key' => '',
+      'placeholder' => '',
+    ] + parent::getDefaultProperties();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginLabel() {
+    return $this->elementManager->isExcluded('webform_location_geocomplete') ? $this->t('Location') : parent::getPluginLabel();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+
+    $form['composite']['app_id'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Algolia application id'),
+      '#description' => $this->t('Algolia requires users to use a valid application id and API key for more than 1,000 requests per day. By <a href="https://www.algolia.com/users/sign_up/places">signing up</a>, you can create a free Places app and access your API keys.'),
+    ];
+    $default_app_id = \Drupal::config('webform.settings')->get('element.default_algolia_places_app_id');
+    if ($default_app_id) {
+      $form['composite']['app_id']['#description'] .= '<br /><br />' . $this->t('Defaults to: %value', ['%value' => $default_app_id]);
+    }
+    else {
+      $form['composite']['app_id']['#required'] = TRUE;
+      if (\Drupal::currentUser()->hasPermission('administer webform')) {
+        $t_args = [':href' => UrlGenerator::fromRoute('webform.config.elements')->toString()];
+        $form['composite']['app_id']['#description'] .= '<br /><br />' . $this->t('You can either enter an element specific application id and API key here or set the <a href=":href">default site-wide application id and API key</a>.', $t_args);
+      }
+    }
+    $form['composite']['api_key'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Algolia API key'),
+    ];
+    $default_api_key = \Drupal::config('webform.settings')->get('element.default_algolia_places_api_key');
+    if ($default_api_key) {
+      $form['composite']['api_key']['#description'] = $this->t('Defaults to: %value', ['%value' => $default_api_key]);
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTestValues(array $element, WebformInterface $webform, array $options = []) {
+    return [
+      [
+        'value' => '1600 Pennsylvania Avenue, Washington, District of Columbia, United States of America',
+        'lat' => '38.8635',
+        'lng' => '-76.946',
+        'name' => '1600 Pennsylvania Avenue',
+        'city' => 'Washington',
+        'country' => 'United States of America',
+        'country_code' => 'us',
+        'administrative' => 'District of Columbia',
+        'county' => 'Prince George\'s County',
+        'suburb' => '',
+        'postcode' => '20020',
+      ],
+    ];
+  }
+
+}
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformManagedFileBase.php b/web/modules/webform/src/Plugin/WebformElement/WebformManagedFileBase.php
index ba0968acde3f5a9e0cc3558ff9f962a103f64f8f..59e47e50f63f150321b57b92a807ab995f278441 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformManagedFileBase.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformManagedFileBase.php
@@ -2,35 +2,43 @@
 
 namespace Drupal\webform\Plugin\WebformElement;
 
+use Drupal\Component\Utility\Bytes;
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Unicode;
+use Drupal\Component\Transliteration\TransliterationInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Link;
+use Drupal\Core\Render\Element;
 use Drupal\Core\Url as UrlGenerator;
 use Drupal\Core\Render\ElementInfoManagerInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
-use Drupal\Component\Transliteration\TransliterationInterface;
 use Drupal\file\Entity\File;
 use Drupal\file\FileInterface;
+use Drupal\webform\Element\WebformHtmlEditor;
 use Drupal\webform\Entity\WebformSubmission;
+use Drupal\webform\Plugin\WebformElementAttachmentInterface;
 use Drupal\webform\Plugin\WebformElementBase;
-use Drupal\Component\Utility\Bytes;
+use Drupal\webform\Utility\WebformOptionsHelper;
 use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformSubmissionForm;
 use Drupal\webform\WebformSubmissionInterface;
-use Psr\Log\LoggerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\webform\Plugin\WebformElementManagerInterface;
+use Drupal\webform\Plugin\WebformElementEntityReferenceInterface;
 use Drupal\webform\WebformLibrariesManagerInterface;
 use Drupal\webform\WebformTokenManagerInterface;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a base class webform 'managed_file' elements.
  */
-abstract class WebformManagedFileBase extends WebformElementBase {
+abstract class WebformManagedFileBase extends WebformElementBase implements WebformElementAttachmentInterface, WebformElementEntityReferenceInterface {
 
   /**
    * List of blacklisted mime types that must be downloaded.
@@ -99,14 +107,14 @@ abstract class WebformManagedFileBase extends WebformElementBase {
    *   The webform libraries manager.
    * @param \Drupal\Core\File\FileSystemInterface $file_system
    *   The file system service.
-   * @param \Drupal\file\FileUsage\FileUsageInterface|NULL $file_usage
+   * @param \Drupal\file\FileUsage\FileUsageInterface|null $file_usage
    *   The file usage service.
    * @param \Drupal\Component\Transliteration\TransliterationInterface $transliteration
    *   The transliteration service.
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
    *   The language manager.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition,LoggerInterface $logger, ConfigFactoryInterface $config_factory, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, ElementInfoManagerInterface $element_info, WebformElementManagerInterface $element_manager, WebformTokenManagerInterface $token_manager, WebformLibrariesManagerInterface $libraries_manager, FileSystemInterface $file_system, $file_usage, TransliterationInterface $transliteration, LanguageManagerInterface $language_manager) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerInterface $logger, ConfigFactoryInterface $config_factory, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, ElementInfoManagerInterface $element_info, WebformElementManagerInterface $element_manager, WebformTokenManagerInterface $token_manager, WebformLibrariesManagerInterface $libraries_manager, FileSystemInterface $file_system, $file_usage, TransliterationInterface $transliteration, LanguageManagerInterface $language_manager) {
     parent::__construct($configuration, $plugin_id, $plugin_definition, $logger, $config_factory, $current_user, $entity_type_manager, $element_info, $element_manager, $token_manager, $libraries_manager);
 
     $this->fileSystem = $file_system;
@@ -149,6 +157,9 @@ public function getDefaultProperties() {
       'max_filesize' => '',
       'file_extensions' => $file_extensions,
       'file_name' => '',
+      'file_help' => '',
+      'file_preview' => '',
+      'file_placeholder' => '',
       'uri_scheme' => 'private',
       'sanitize' => FALSE,
       'button' => FALSE,
@@ -199,6 +210,13 @@ public function isEnabled() {
     return (empty($scheme_options)) ? FALSE : TRUE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function hasManagedFiles(array $element) {
+    return TRUE;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -212,7 +230,7 @@ public function displayDisabledWarning(array $element) {
       $scheme_options = static::getVisibleStreamWrappers();
       $uri_scheme = $this->getUriScheme($element);
       if (!isset($scheme_options[$uri_scheme]) && $this->currentUser->hasPermission('administer webform')) {
-        drupal_set_message($this->t('The \'File\' element is unavailable because a <a href="https://www.ostraining.com/blog/drupal/creating-drupal-8-private-file-system/">private files directory</a> has not been configured and public file uploads have not been enabled. For more information see: <a href="https://www.drupal.org/psa-2016-003">DRUPAL-PSA-2016-003</a>'), 'warning');
+        $this->messenger()->addWarning($this->t('The \'File\' element is unavailable because a <a href="https://www.ostraining.com/blog/drupal/creating-drupal-8-private-file-system/">private files directory</a> has not been configured and public file uploads have not been enabled. For more information see: <a href="https://www.drupal.org/psa-2016-003">DRUPAL-PSA-2016-003</a>'));
         $context = [
           'link' => Link::fromTextAndUrl($this->t('Edit'), UrlGenerator::fromRoute('<current>'))->toString(),
         ];
@@ -248,24 +266,57 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       $element['#upload_location'] = $this->getUploadLocation($element, $webform_submission->getWebform());
     }
 
-    $element['#upload_validators']['file_validate_size'] = [$this->getMaxFileSize($element)];
-    $element['#upload_validators']['file_validate_extensions'] = [$this->getFileExtensions($element)];
-
-    // Use custom validation callback so that File entities can be converted
-    // into file ids (akk fids).
+    // Get file limit.
+    $file_limit = $webform_submission->getWebform()->getSetting('form_file_limit')
+        ?: \Drupal::config('webform.settings')->get('settings.default_form_file_limit')
+        ?: '';
+
+    // Validate callbacks.
+    $element_validate = [];
+    // Convert File entities into file ids (akk fids).
+    $element_validate[] = [get_class($this), 'validateManagedFile'];
+    // Check file upload limit.
+    if ($file_limit) {
+      $element_validate[] = [get_class($this), 'validateManagedFileLimit'];
+    }
     // NOTE: Using array_splice() to make sure that self::validateManagedFile
     // is executed before all other validation hooks are executed but after
     // \Drupal\file\Element\ManagedFile::validateManagedFile.
-    array_splice($element['#element_validate'], 1, 0, [[get_class($this), 'validateManagedFile']]);
+    array_splice($element['#element_validate'], 1, 0, $element_validate);
+
+    // Upload validators.
+    $element['#upload_validators']['file_validate_size'] = [$this->getMaxFileSize($element)];
+    $element['#upload_validators']['file_validate_extensions'] = [$this->getFileExtensions($element)];
 
-    // Add file upload help to the element.
-    $element['help'] = [
+    // Add file upload help to the element as #description, #help, or #more.
+    // Copy upload validator so that we can add webform's file limit to
+    // file upload help only.
+    $upload_validators = $element['#upload_validators'];
+    if ($file_limit) {
+      $upload_validators['webform_file_limit'] = [Bytes::toInt($file_limit)];
+    }
+    $file_upload_help = [
       '#theme' => 'file_upload_help',
-      '#upload_validators' => $element['#upload_validators'],
+      '#upload_validators' => $upload_validators,
       '#cardinality' => (empty($element['#multiple'])) ? 1 : $element['#multiple'],
-      '#prefix' => '<div class="description">',
-      '#suffix' => '</div>',
     ];
+    $file_help = (isset($element['#file_help'])) ? $element['#file_help'] : 'description';
+    if ($file_help !== 'none') {
+      if (isset($element["#$file_help"])) {
+        if (is_array($element["#$file_help"])) {
+          $file_help_content = $element["#$file_help"];
+        }
+        else {
+          $file_help_content = ['#markup' => $element["#$file_help"]];
+        }
+        $file_help_content += ['#suffix' => '<br/>'];
+        $element["#$file_help"] = ['content' => $file_help_content];
+      }
+      else {
+        $element["#$file_help"] = [];
+      }
+      $element["#$file_help"]['file_upload_help'] = $file_upload_help;
+    }
 
     // Issue #2705471: Webform states File fields.
     // Workaround: Wrap the 'managed_file' element in a basic container.
@@ -278,6 +329,17 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       $container[$element['#webform_key']] = $element + ['#webform_managed_file_processed' => TRUE];
       $element = $container;
     }
+
+    // Add process callback.
+    // Set element's #process callback so that is not replaced by
+    // additional #process callbacks.
+    $this->setElementDefaultCallback($element, 'process');
+    $element['#process'][] = [get_class($this), 'processManagedFile'];
+
+    // Add managed file upload tracking.
+    if (\Drupal::moduleHandler()->moduleExists('file')) {
+      $element['#attached']['library'][] = 'webform/webform.element.managed_file';
+    }
   }
 
   /**
@@ -295,6 +357,11 @@ public function setDefaultValue(array &$element) {
   protected function formatHtmlItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
     $value = $this->getValue($element, $webform_submission, $options);
     $file = $this->getFile($element, $value, $options);
+
+    if (empty($file)) {
+      return '';
+    }
+
     $format = $this->getItemFormat($element);
     switch ($format) {
       case 'id':
@@ -331,6 +398,11 @@ protected function formatHtmlItem(array $element, WebformSubmissionInterface $we
   protected function formatTextItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
     $value = $this->getValue($element, $webform_submission, $options);
     $file = $this->getFile($element, $value, $options);
+
+    if (empty($file)) {
+      return '';
+    }
+
     $format = $this->getItemFormat($element);
     switch ($format) {
       case 'id':
@@ -404,11 +476,11 @@ protected function getFile(array $element, $value, array $options) {
    * @return array
    *   An associative array containing files.
    */
-  protected function getFiles(array $element, $value, array $options) {
+  protected function getFiles(array $element, $value, array $options = []) {
     if (empty($value)) {
       return [];
     }
-    return $this->entityTypeManager->getStorage('file')->loadMultiple($value);
+    return $this->entityTypeManager->getStorage('file')->loadMultiple((array) $value);
   }
 
   /**
@@ -444,60 +516,56 @@ public function postSave(array &$element, WebformSubmissionInterface $webform_su
 
     // Delete the old file uploads.
     $delete_fids = array_diff($original_fids, $fids);
-    $this->deleteFiles($delete_fids, $webform_submission);
+    static::deleteFiles($webform_submission, $delete_fids);
 
-    // Exit if there is no fids.
-    if (empty($fids)) {
-      return;
-    }
-
-    /** @var \Drupal\file\FileInterface[] $files */
-    $files = $this->entityTypeManager->getStorage('file')->loadMultiple($fids);
-    foreach ($files as $file) {
-      $source_uri = $file->getFileUri();
-      $destination_uri = $this->getFileDestinationUri($element, $file, $webform_submission);
-
-      // Save file if there is a new destination URI.
-      if ($source_uri != $destination_uri) {
-        $destination_uri = file_unmanaged_move($source_uri, $destination_uri);
-        $file->setFileUri($destination_uri);
-        $file->setFileName($this->fileSystem->basename($destination_uri));
-        $file->save();
-      }
-
-      // Update file usage table.
-      // Setting file usage will also make the file's status permanent.
-      $this->fileUsage->delete($file, 'webform', 'webform_submission', $webform_submission->id());
-      $this->fileUsage->add($file, 'webform', 'webform_submission', $webform_submission->id());
-    }
+    // Add new files.
+    $this->addFiles($element, $webform_submission, $fids);
   }
 
   /**
    * {@inheritdoc}
    */
   public function postDelete(array &$element, WebformSubmissionInterface $webform_submission) {
-    $fids = (array) ($webform_submission->getElementData($element['#webform_key']) ?: []);
-    $this->deleteFiles($fids, $webform_submission);
+    // Uploaded files are deleted via the webform submission.
+    // This ensures that all files associated with a submission are deleted.
+    // @see \Drupal\webform\WebformSubmissionStorage::delete
   }
 
   /**
    * {@inheritdoc}
    */
   public function getTestValues(array $element, WebformInterface $webform, array $options = []) {
-    if ($this->isDisabled() || !isset($element['#webform_key'])) {
+    if ($this->isDisabled()) {
       return NULL;
     }
 
+    // Get element or composite key.
+    if (isset($element['#webform_key'])) {
+      $key = $element['#webform_key'];
+    }
+    elseif (isset($element['#webform_composite_key'])) {
+      $key = $element['#webform_composite_key'];
+    }
+    else {
+      return NULL;
+    }
+
+    // Append delta to key.
+    // @see \Drupal\webform\Plugin\WebformElement\WebformCompositeBase::getTestValues
+    if (isset($options['delta'])) {
+      $key .= '_' . $options['delta'];
+    }
+
     $file_extensions = explode(' ', $this->getFileExtensions($element));
     $file_extension = $file_extensions[array_rand($file_extensions)];
     $upload_location = $this->getUploadLocation($element, $webform);
-    $file_destination = $upload_location . '/' . $element['#webform_key'] . '.' . $file_extension;
+    $file_destination = $upload_location . '/' . $key . '.' . $file_extension;
 
     // Look for an existing temp files that have not been uploaded.
     $fids = $this->entityTypeManager->getStorage('file')->getQuery()
       ->condition('status', FALSE)
       ->condition('uid', $this->currentUser->id())
-      ->condition('uri', $upload_location . '/' . $element['#webform_key'] . '.%', 'LIKE')
+      ->condition('uri', $upload_location . '/' . $key . '.%', 'LIKE')
       ->execute();
     if ($fids) {
       return reset($fids);
@@ -532,29 +600,34 @@ public function getTestValues(array $element, WebformInterface $webform, array $
    *   Max file size.
    */
   protected function getMaxFileSize(array $element) {
-    // Set max file size.
     $max_filesize = $this->configFactory->get('webform.settings')->get('file.default_max_filesize') ?: file_upload_max_size();
     $max_filesize = Bytes::toInt($max_filesize);
     if (!empty($element['#max_filesize'])) {
-      $max_filesize = min($max_filesize, Bytes::toInt($element['#max_filesize']) * 1024 * 1024);
+      $max_filesize = min($max_filesize, Bytes::toInt($element['#max_filesize'] . 'MB'));
     }
     return $max_filesize;
   }
 
   /**
-   * Get allowed file extensions for an element.
+   * Get the allowed file extensions for an element.
    *
    * @param array $element
    *   An element.
    *
    * @return int
-   *   File extension.
+   *   File extensions.
    */
   protected function getFileExtensions(array $element = NULL) {
-    if (!empty($element['#file_extensions'])) {
-      return $element['#file_extensions'];
-    }
+    return (!empty($element['#file_extensions'])) ? $element['#file_extensions'] : $this->getDefaultFileExtensions();
+  }
 
+  /**
+   * Get the default allowed file extensions.
+   *
+   * @return int
+   *   File extensions.
+   */
+  protected function getDefaultFileExtensions() {
     $file_type = str_replace('webform_', '', $this->getPluginId());
     return $this->configFactory->get('webform.settings')->get("file.default_{$file_type}_extensions");
   }
@@ -616,6 +689,121 @@ protected function getUriScheme(array $element) {
     }
   }
 
+  /**
+   * Process callback  for managed file elements.
+   */
+  public static function processManagedFile(&$element, FormStateInterface $form_state, &$complete_form) {
+    // Disable inline form errors for multiple file upload checkboxes.
+    if (!empty($element['#multiple'])) {
+      foreach (Element::children($element) as $key) {
+        if (isset($element[$key]['selected'])) {
+          $element[$key]['selected']['#error_no_message'] = TRUE;
+        }
+      }
+    }
+
+    // Preview uploaded file.
+    if (!empty($element['#file_preview'])) {
+      // Get the element's plugin object.
+      /** @var \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager */
+      $element_manager = \Drupal::service('plugin.manager.webform.element');
+      /** @var \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase $element_plugin */
+      $element_plugin = $element_manager->getElementInstance($element);
+
+      // Get the webform submission.
+      /** @var \Drupal\webform\WebformSubmissionForm $form_object */
+      $form_object = $form_state->getFormObject();
+      /** @var \Drupal\webform\webform_submission $webform_submission */
+      $webform_submission = $form_object->getEntity();
+
+      // Create a temporary preview element with an overridden #format.
+      $preview_element = ['#format' => $element['#file_preview']] + $element;
+
+      // Convert '#theme': file_link to a container with a file preview.
+      $delta = 0;
+      foreach (Element::children($element) as $child_key) {
+        if (strpos($child_key, 'file_') !== 0) {
+          continue;
+        }
+
+        // Set multiple options delta.
+        $options = ['delta' => $delta];
+        $delta++;
+
+        $fid = str_replace('file_', '', $child_key);
+        $file = File::load((string) $fid);
+        // Make sure the file entity exists.
+        if (!$file) {
+          continue;
+        }
+
+        // Don't allow anonymous temporary files to be previewed.
+        // @see template_preprocess_file_link().
+        // @see webform_preprocess_file_link().
+        if ($file->isTemporary() && $file->getOwner()->isAnonymous() && strpos($file->getFileUri(), 'private://') === 0) {
+          continue;
+        }
+
+        $preview = $element_plugin->previewManagedFile($preview_element, $webform_submission, $options);
+        if (isset($element[$child_key]['filename'])) {
+          // Single file.
+          // Covert file link to a container with preview.
+          unset($element[$child_key]['filename']['#theme']);
+          $element[$child_key]['filename']['#type'] = 'container';
+          $element[$child_key]['filename']['#attributes']['class'][] = 'webform-managed-file-preview';
+          $element[$child_key]['filename']['#attributes']['class'][] = Html::getClass($element['#type'] . '-preview');
+          $element[$child_key]['filename']['preview'] = $preview;
+        }
+        elseif (isset($element[$child_key]['selected'])) {
+          // Multiple files.
+          // Convert file link checkbox #title to preview.
+          $element[$child_key]['selected']['#wrapper_attributes']['class'][] = 'webform-managed-file-preview-wrapper';
+          $element[$child_key]['selected']['#wrapper_attributes']['class'][] = Html::getClass($element['#type'] . '-preview-wrapper');
+          $element[$child_key]['selected']['#label_attributes']['class'][] = 'webform-managed-file-preview';
+          $element[$child_key]['selected']['#label_attributes']['class'][] = Html::getClass($element['#type'] . '-preview');
+
+          $element[$child_key]['selected']['#title'] = \Drupal::service('renderer')->render($preview);
+        }
+      }
+    }
+
+    // File placeholder.
+    if (!empty($element['#file_placeholder']) && (empty($element['#value']) || empty($element['#value']['fids']))) {
+      $element['file_placeholder'] = [
+        '#type' => 'container',
+        '#attributes' => [
+          'class' => [
+            'webform-managed-file-placeholder',
+            Html::getClass($element['#type'] . '-placeholder'),
+          ],
+        ],
+        // Display placeholder before file upload input.
+        '#weight' => ($element['upload']['#weight'] - 1),
+        'content' => WebformHtmlEditor::checkMarkup($element['#file_placeholder']),
+      ];
+    }
+
+    return $element;
+  }
+
+  /**
+   * Preview a managed file element upload.
+   *
+   * @param array $element
+   *   An element.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   * @param array $options
+   *   An array of options.
+   *
+   * @return string|array
+   *   A preview.
+   */
+  public function previewManagedFile(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
+    $build = $this->formatHtmlItem($element, $webform_submission, $options);
+    return (is_array($build)) ? $build : ['#markup' => $build];
+  }
+
   /**
    * Form API callback. Consolidate the array of fids for this field into a single fids.
    */
@@ -634,6 +822,63 @@ public static function validateManagedFile(array &$element, FormStateInterface $
     }
   }
 
+  /**
+   * Form API callback. Validate file upload limit.
+   *
+   * @see \Drupal\webform\WebformSubmissionForm::validateForm
+   */
+  public static function validateManagedFileLimit(array &$element, FormStateInterface $form_state, &$complete_form) {
+    // Set empty files to NULL and exit.
+    if (empty($element['#files'])) {
+      return;
+    }
+
+    // Only validate file limits for ajax uploads.
+    $wrapper_format = \Drupal::request()->get(MainContentViewSubscriber::WRAPPER_FORMAT);
+    if (!$wrapper_format || !in_array($wrapper_format, ['drupal_ajax', 'drupal_modal', 'drupal_dialog'])) {
+      return;
+    }
+
+    $fids = array_keys($element['#files']);
+
+    // Get WebformSubmissionForm object.
+    $form_object = $form_state->getFormObject();
+    if (!($form_object instanceof WebformSubmissionForm)) {
+      return;
+    }
+
+    // Skip validation when removing file upload.
+    $trigger_element = $form_state->getTriggeringElement();
+    $op = (string) $trigger_element['#value'];
+    if (in_array($op, [(string) t('Remove'), (string) t('Remove selected')])) {
+      return;
+    }
+
+    // Get file upload limit.
+    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
+    $webform_submission = $form_object->getEntity();
+    $file_limit = $webform_submission->getWebform()->getSetting('form_file_limit')
+      ?: \Drupal::config('webform.settings')->get('settings.default_form_file_limit')
+      ?: '';
+    $file_limit = Bytes::toInt($file_limit);
+
+    // Track file size across all file upload elements.
+    static $total_file_size = 0;
+    /** @var \Drupal\file\FileInterface[] $files */
+    $files = File::loadMultiple($fids);
+    foreach ($files as $file) {
+      $total_file_size += (int) $file->getSize();
+    }
+
+    // If has access and total file size exceeds file limit then display error.
+    $has_access = (!isset($element['#access']) || $element['#access']);
+    if ($has_access && $total_file_size > $file_limit) {
+      $t_args = ['%quota' => format_size($file_limit)];
+      $message = t("This form's file upload quota of %quota has been exceeded. Please remove some files.", $t_args);
+      $form_state->setError($element, $message);
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -678,7 +923,7 @@ public function form(array $form, FormStateInterface $form_state) {
     $scheme_options = static::getVisibleStreamWrappers();
     $form['file']['uri_scheme'] = [
       '#type' => 'radios',
-      '#title' => t('Upload destination'),
+      '#title' => t('File upload destination'),
       '#description' => t('Select where the final files should be stored. Private file storage has more overhead than public files, but allows restricted access to files within this element.'),
       '#required' => TRUE,
       '#options' => $scheme_options,
@@ -711,6 +956,31 @@ public function form(array $form, FormStateInterface $form_state) {
     $max_filesize = \Drupal::config('webform.settings')->get('file.default_max_filesize') ?: file_upload_max_size();
     $max_filesize = Bytes::toInt($max_filesize);
     $max_filesize = ($max_filesize / 1024 / 1024);
+    $form['file']['file_help'] = [
+      '#type' => 'select',
+      '#title' => $this->t('File upload help display'),
+      '#description' => $this->t('Determines the placement of the file upload help .'),
+      '#options' => [
+        '' => $this->t('Description'),
+        'help' => $this->t('Help'),
+        'more' => $this->t('More'),
+        'none' => $this->t('None'),
+      ],
+    ];
+    $form['file']['file_placeholder'] = [
+      '#type' => 'webform_html_editor',
+      '#title' => $this->t('File upload placeholder'),
+      '#description' => $this->t('The placeholder will be shown before a file is uploaded.'),
+    ];
+    $form['file']['file_preview'] = [
+      '#type' => 'select',
+      '#title' => $this->t('File upload preview (Authenticated users only)'),
+      '#description' => $this->t('Select how the uploaded file previewed.') . '<br/><br/>' .
+          $this->t('Allowing anonymous users to preview files is dangerous.') . '<br/>' .
+          $this->t('For more information see: <a href="https://www.drupal.org/psa-2016-003">DRUPAL-PSA-2016-003</a>'),
+      '#options' => WebformOptionsHelper::appendValueToText($this->getItemFormats()),
+      '#empty_option' => '<' . $this->t('no preview') . '>',
+    ];
     $form['file']['max_filesize'] = [
       '#type' => 'number',
       '#title' => $this->t('Maximum file size'),
@@ -719,11 +989,13 @@ public function form(array $form, FormStateInterface $form_state) {
       '#description' => $this->t('Enter the max file size a user may upload.'),
       '#min' => 1,
       '#max' => $max_filesize,
+      '#step' => 'any',
     ];
     $form['file']['file_extensions'] = [
       '#type' => 'textfield',
       '#title' => $this->t('Allowed file extensions'),
-      '#description' => $this->t('Separate extensions with a space and do not include the leading dot.'),
+      '#description' => $this->t('Separate extensions with a space and do not include the leading dot.') . '<br/><br/>' .
+        $this->t('Defaults to: %value', ['%value' => $this->getDefaultFileExtensions()]),
       '#maxlength' => 255,
     ];
     $form['file']['file_name'] = [
@@ -751,10 +1023,12 @@ public function form(array $form, FormStateInterface $form_state) {
       '#message_type' => 'warning',
       '#message_message' => $this->t('For security reasons we advise to use %file_rename together with the %sanitization option.', $t_args),
       '#access' => TRUE,
-      '#states' => ['visible' => [
-        ':input[name="properties[file_name][checkbox]"]' => ['checked' => TRUE],
-        ':input[name="properties[sanitize]"]' => ['checked' => FALSE],
-      ]],
+      '#states' => [
+        'visible' => [
+          ':input[name="properties[file_name][checkbox]"]' => ['checked' => TRUE],
+          ':input[name="properties[sanitize]"]' => ['checked' => FALSE],
+        ],
+      ],
     ];
     $form['file']['multiple'] = [
       '#type' => 'checkbox',
@@ -773,7 +1047,7 @@ public function form(array $form, FormStateInterface $form_state) {
     ];
     $form['file']['button__title'] = [
       '#type' => 'textfield',
-      '#title' => $this->t('Upload button title'),
+      '#title' => $this->t('File upload button title'),
       '#description' => $this->t('Defaults to: %value', ['%value' => $this->t('Choose file')]),
       '#states' => [
         'visible' => [
@@ -783,7 +1057,7 @@ public function form(array $form, FormStateInterface $form_state) {
     ];
     $form['file']['button__attributes'] = [
       '#type' => 'webform_element_attributes',
-      '#title' => $this->t('Upload button attributes'),
+      '#title' => $this->t('File upload button'),
       '#classes' => $this->configFactory->get('webform.settings')->get('settings.button_classes'),
       '#class__description' => $this->t("Apply classes to the button. Button classes default to 'button button-primary'."),
       '#states' => [
@@ -806,25 +1080,44 @@ public function form(array $form, FormStateInterface $form_state) {
    * In Drupal 8.4.x+ unused webform managed files are no longer
    * marked as temporary.
    *
-   * @param array $fids
-   *   An array of file ids.
    * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
    *   A webform submission.
+   * @param null|array $fids
+   *   An array of file ids. If NULL all files are deleted.
    */
-  protected function deleteFiles(array $fids, WebformSubmissionInterface $webform_submission) {
+  public static function deleteFiles(WebformSubmissionInterface $webform_submission, array $fids = NULL) {
+    // Make sure the file.module is enabled since this method is called from
+    // \Drupal\webform\WebformSubmissionStorage::delete.
+    if (!\Drupal::moduleHandler()->moduleExists('file')) {
+      return;
+    }
+
+    if ($fids === NULL) {
+      $fids = \Drupal::database()->select('file_usage', 'fu')
+        ->fields('fu', ['fid'])
+        ->condition('module', 'webform')
+        ->condition('type', 'webform_submission')
+        ->condition('id', $webform_submission->id())
+        ->execute()
+        ->fetchCol();
+    }
+
     if (empty($fids)) {
       return;
     }
 
+    /** @var \Drupal\file\FileUsage\FileUsageInterface $file_usage */
+    $file_usage = \Drupal::service('file.usage');
+
     $make_unused_managed_files_temporary = \Drupal::config('webform.settings')->get('file.make_unused_managed_files_temporary');
     $delete_temporary_managed_files = \Drupal::config('webform.settings')->get('file.delete_temporary_managed_files');
 
     /** @var \Drupal\file\FileInterface[] $files */
     $files = File::loadMultiple($fids);
     foreach ($files as $file) {
-      $this->fileUsage->delete($file, 'webform', 'webform_submission', $webform_submission->id());
+      $file_usage->delete($file, 'webform', 'webform_submission', $webform_submission->id());
       // Make unused files temporary.
-      if ($make_unused_managed_files_temporary && empty($this->fileUsage->listUsage($file)) && !$file->isTemporary()) {
+      if ($make_unused_managed_files_temporary && empty($file_usage->listUsage($file)) && !$file->isTemporary()) {
         $file->setTemporary();
         $file->save();
       }
@@ -839,6 +1132,48 @@ protected function deleteFiles(array $fids, WebformSubmissionInterface $webform_
     }
   }
 
+  /**
+   * Add a webform submission file's usage and mark it as permanent.
+   *
+   * @param array $element
+   *   An element.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   * @param array $fids
+   *   An array of file ids.
+   */
+  public function addFiles(array $element, WebformSubmissionInterface $webform_submission, array $fids) {
+    // Make sure the file.module is enabled since this method is called from
+    // \Drupal\webform\Plugin\WebformElement\WebformCompositeBase::postSave.
+    if (!\Drupal::moduleHandler()->moduleExists('file')) {
+      return;
+    }
+    // Make sure there are files that need to added.
+    if (empty($fids)) {
+      return;
+    }
+
+    /** @var \Drupal\file\FileInterface[] $files */
+    $files = $this->entityTypeManager->getStorage('file')->loadMultiple($fids);
+    foreach ($files as $file) {
+      $source_uri = $file->getFileUri();
+      $destination_uri = $this->getFileDestinationUri($element, $file, $webform_submission);
+
+      // Save file if there is a new destination URI.
+      if ($source_uri != $destination_uri) {
+        $destination_uri = file_unmanaged_move($source_uri, $destination_uri);
+        $file->setFileUri($destination_uri);
+        $file->setFileName($this->fileSystem->basename($destination_uri));
+        $file->save();
+      }
+
+      // Update file usage table.
+      // Setting file usage will also make the file's status permanent.
+      $this->fileUsage->delete($file, 'webform', 'webform_submission', $webform_submission->id());
+      $this->fileUsage->add($file, 'webform', 'webform_submission', $webform_submission->id());
+    }
+  }
+
   /**
    * Determine the destination URI where to save an uploaded file.
    *
@@ -871,18 +1206,28 @@ protected function getFileDestinationUri(array $element, FileInterface $file, We
 
     // Sanitize filename.
     // @see http://stackoverflow.com/questions/2021624/string-sanitizer-for-filename
+    // @see \Drupal\webform_attachment\Element\WebformAttachmentBase::getFileName
     if (!empty($element['#sanitize'])) {
-      $destination_extension = Unicode::strtolower($destination_extension);
+      $destination_extension = mb_strtolower($destination_extension);
 
       $destination_basename = substr(pathinfo($destination_filename, PATHINFO_BASENAME), 0, -strlen(".$destination_extension"));
-      $destination_basename = Unicode::strtolower($destination_basename);
+      $destination_basename = mb_strtolower($destination_basename);
       $destination_basename = $this->transliteration->transliterate($destination_basename, $this->languageManager->getCurrentLanguage()->getId(), '-');
       $destination_basename = preg_replace('([^\w\s\d\-_~,;:\[\]\(\].]|[\.]{2,})', '', $destination_basename);
       $destination_basename = preg_replace('/\s+/', '-', $destination_basename);
       $destination_basename = trim($destination_basename, '-');
-      // If the basename if empty use the element's key.
+
+      // If the basename is empty use the element's key, composite key, or type.
       if (empty($destination_basename)) {
-        $destination_basename = $element['#webform_key'];
+        if (isset($element['#webform_key'])) {
+          $destination_basename = $element['#webform_key'];
+        }
+        elseif (isset($element['#webform_composite_key'])) {
+          $destination_basename = $element['#webform_composite_key'];
+        }
+        else {
+          $destination_basename = $element['#type'];
+        }
       }
 
       $destination_filename = $destination_basename . '.' . $destination_extension;
@@ -950,9 +1295,15 @@ public static function accessFileDownload($uri) {
         // Return file content headers.
         $headers = file_get_content_headers($file);
 
-        // Force blacklisted files to be downloaded.
+        /** @var \Drupal\Core\File\FileSystemInterface $file_system */
+        $file_system = \Drupal::service('file_system');
+        $filename = $file_system->basename($uri);
+        // Force blacklisted files to be downloaded instead of opening in the browser.
         if (in_array($headers['Content-Type'], static::$blacklistedMimeTypes)) {
-          $headers['Content-Disposition'] = 'attachment';
+          $headers['Content-Disposition'] = 'attachment; filename="' . Unicode::mimeHeaderEncode($filename) . '"';
+        }
+        else {
+          $headers['Content-Disposition'] = 'inline; filename="' . Unicode::mimeHeaderEncode($filename) . '"';
         }
 
         return $headers;
@@ -975,4 +1326,53 @@ public static function getVisibleStreamWrappers() {
     return $stream_wrappers;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getTargetType(array $element) {
+    return 'file';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTargetEntity(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
+    $value = $this->getValue($element, $webform_submission, $options);
+    if (empty($value)) {
+      return NULL;
+    }
+    return $this->getFile($element, $value, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTargetEntities(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
+    $value = $this->getValue($element, $webform_submission, $options);
+    if (empty($value)) {
+      return NULL;
+    }
+    return $this->getFiles($element, $value, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAttachments(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
+    $attachments = [];
+    $files = $this->getTargetEntities($element, $webform_submission, $options);
+    foreach ($files as $file) {
+      $attachments[] = [
+        'filecontent' => file_get_contents($file->getFileUri()),
+        'filename' => $file->getFilename(),
+        'filemime' => $file->getMimeType(),
+        'filepath' => \Drupal::service('file_system')->realpath($file->getFileUri()),
+        // URI is used when debugging or resending messages.
+        // @see \Drupal\webform\Plugin\WebformHandler\EmailWebformHandler::buildAttachments
+        '_uri' => file_create_url($file->getFileUri()),
+      ];
+    }
+    return $attachments;
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformMapping.php b/web/modules/webform/src/Plugin/WebformElement/WebformMapping.php
index a4d02319b3de7adb71c4a46df623181f8a2b3ec0..5e4c8d0346ecd87e2d93395055134ad02f843a6b 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformMapping.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformMapping.php
@@ -37,6 +37,7 @@ public function getDefaultProperties() {
       'default_value' => [],
       // Description/Help.
       'help' => '',
+      'help_title' => '',
       'description' => '',
       'more' => '',
       'more_title' => '',
@@ -51,6 +52,7 @@ public function getDefaultProperties() {
       'format' => $this->getItemDefaultFormat(),
       'format_html' => '',
       'format_text' => '',
+      'format_attributes' => [],
       // Mapping settings.
       'arrow' => '→',
       'source' => [],
@@ -61,6 +63,7 @@ public function getDefaultProperties() {
       'destination__description' => '',
       // Attributes.
       'wrapper_attributes' => [],
+      'label_attributes' => [],
     ] + $this->getDefaultBaseProperties();
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformMarkupBase.php b/web/modules/webform/src/Plugin/WebformElement/WebformMarkupBase.php
index 869aa9fad0139c6b8485610be522c6e39414a6cb..1c78e82d376c366cdff204ebfcded302502f804b 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformMarkupBase.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformMarkupBase.php
@@ -44,6 +44,7 @@ public function getDefaultProperties() {
   protected function getDefaultBaseProperties() {
     $properties = parent::getDefaultBaseProperties();
     unset($properties['prepopulate']);
+    unset($properties['states_clear']);
     return $properties;
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformRating.php b/web/modules/webform/src/Plugin/WebformElement/WebformRating.php
index 1ce2de9a3b8203cb606f31a901bf58fbf27cadef..63793cbb59f3592f9312791f7d0d7530edea77d3 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformRating.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformRating.php
@@ -124,6 +124,15 @@ public function form(array $form, FormStateInterface $form_state) {
       '#description' => $this->t('If checked, a reset button will be placed before the rating element.'),
       '#return_value' => TRUE,
     ];
+
+    // Only allow a rating element to be required if the min value can be
+    // set to 0.
+    $form['validation']['required_container']['#states'] = [
+      'visible' => [
+        ':input[name="properties[min]"]' => ['value' => '0'],
+      ],
+    ];
+
     return $form;
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformSection.php b/web/modules/webform/src/Plugin/WebformElement/WebformSection.php
index 061a55de17b4dcda5a49945aa9dff29966dee541..b4545f29521610751120a378634b6de3b0f50c72 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformSection.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformSection.php
@@ -22,7 +22,13 @@ class WebformSection extends ContainerBase {
    */
   public function getDefaultProperties() {
     return [
+      // Description/Help.
       'help' => '',
+      'help_title' => '',
+      'description' => '',
+      'more' => '',
+      'more_title' => '',
+      // Title.
       'title_tag' => \Drupal::config('webform.settings')->get('element.default_section_title_tag'),
       'title_display' => '',
     ] + parent::getDefaultProperties();
@@ -55,6 +61,7 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['form']['title_tag'] = [
       '#type' => 'webform_select_other',
       '#title' => $this->t('Title tag'),
+      '#description' => $this->t("The section's title HTML tag."),
       '#options' => [
         'h1' => $this->t('Header 1 (h1)'),
         'h2' => $this->t('Header 2 (h2)'),
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformTableTrait.php b/web/modules/webform/src/Plugin/WebformElement/WebformTableTrait.php
index 3d5bd0da6a0d81266eaa3df6217bc77b1288e7a9..778a9aaf4433d3e1a731997a59d7815cc8a391ba 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformTableTrait.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformTableTrait.php
@@ -3,6 +3,8 @@
 namespace Drupal\webform\Plugin\WebformElement;
 
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\OptGroup;
+use Drupal\webform\Utility\WebformArrayHelper;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
@@ -14,10 +16,6 @@ trait WebformTableTrait {
    * {@inheritdoc}
    */
   public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
-    if ($this->hasMultipleValues($element)) {
-      $element['#element_validate'][] = [get_class($this), 'validateMultipleOptions'];
-    }
-
     parent::prepare($element, $webform_submission);
 
     // Add missing element class.
@@ -42,6 +40,12 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
     }
 
     $element['#attached']['library'][] = 'webform/webform.element.' . $element['#type'];
+
+    // Set table select element's #process callback so that fix UX
+    // and accessiblity issues.
+    if ($this->getPluginId() === 'tableselect') {
+      static::setProcessTableSelectCallback($element);
+    }
   }
 
   /**
@@ -75,16 +79,126 @@ public function form(array $form, FormStateInterface $form_state) {
   protected function getTableSelectElementSelectorOptions(array $element, $input_selector = '') {
     $title = $this->getAdminLabel($element) . ' [' . $this->getPluginLabel() . ']';
     $name = $element['#webform_key'];
-    $type = ($this->hasMultipleValues($element) ? $this->t('Checkbox') : $this->t('Radio'));
+    if ($this->hasMultipleValues($element)) {
+      $selectors = [];
+      foreach ($element['#options'] as $value => $text) {
+        if (is_array($text)) {
+          $text = $value;
+        }
+        $selectors[":input[name=\"{$name}[{$value}]$input_selector\"]"] = $text . ' [' . $this->t('Checkbox') . ']';
+      }
+      return [$title => $selectors];
+    }
+    else {
+      return [":input[name=\"{$name}\"]" => $title];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorSourceValues(array $element) {
+    if ($this->hasMultipleValues($element)) {
+      return [];
+    }
+
+    $name = $element['#webform_key'];
+    $options = OptGroup::flattenOptions($element['#options']);
+    return [":input[name=\"{$name}\"]" => $options];
+  }
 
-    $selectors = [];
-    foreach ($element['#options'] as $value => $text) {
-      if (is_array($text)) {
-        $text = $value;
+  /**
+   * Process table select and attach JavaScript.
+   *
+   * @param array $element
+   *   An associative array containing the properties and children of
+   *   the tableselect element.
+   *
+   * @return array
+   *   The processed element.
+   *
+   * @see \Drupal\Core\Render\Element\Tableselect::processTableselect
+   */
+  public static function processTableSelect(array $element) {
+    $element['#attributes']['class'][] = 'webform-tableselect';
+    $element['#attributes']['class'][] = 'js-webform-tableselect';
+    $element['#attached']['library'][] = 'webform/webform.element.tableselect';
+    return $element;
+  }
+
+  /**
+   * Process table selected options and add #title to the table's options.
+   *
+   * @param array $element
+   *   An associative array containing the properties and children of
+   *   the tableselect element.
+   *
+   * @return array
+   *   The processed element.
+   *
+   * @see \Drupal\Core\Render\Element\Tableselect::processTableselect
+   */
+  public static function processTableSelectOptions(array $element) {
+    foreach ($element['#options'] as $key => $choice) {
+      if (isset($element[$key]) && empty($element[$key]['#title'])) {
+        if ($title = static::getTableSelectOptionTitle($choice)) {
+          $element[$key]['#title'] = $title;
+          $element[$key]['#title_display'] = 'invisible';
+        }
+      }
+    }
+    return $element;
+  }
+
+  /**
+   * Set process table select element callbacks.
+   *
+   * @param array $element
+   *   An associative array containing the properties and children of
+   *   the table select element.
+   *
+   * @see \Drupal\Core\Render\Element\Tableselect::processTableselect
+   */
+  public static function setProcessTableSelectCallback(array &$element) {
+    $class = get_called_class();
+    $element['#process'] = [
+      ['\Drupal\Core\Render\Element\Tableselect', 'processTableselect'],
+      [$class , 'processTableSelect'],
+      [$class , 'processTableSelectOptions'],
+    ];
+  }
+
+  /**
+   * Get table selection option title/text.
+   *
+   * Issue #2719453: Tableselect single radio button missing #title attribute
+   * and is not accessible,
+   *
+   * @param array $option
+   *   A table select option.
+   *
+   * @return string|\Drupal\Component\Render\MarkupInterface|null
+   *   Table selection option title/text.
+   *
+   * @see https://www.drupal.org/project/drupal/issues/2719453
+   */
+  public static function getTableSelectOptionTitle(array $option) {
+    if (is_array($option) && WebformArrayHelper::isAssociative($option)) {
+      // Get first value from custom options.
+      $title = reset($option);
+      if (is_array($title)) {
+        $title = \Drupal::service('renderer')->render($title);
       }
-      $selectors[":input[name=\"{$name}[{$value}]$input_selector\"]"] = $text . ' [' . $type . ']';
+      return $title;
+    }
+    elseif (is_array($option) && !empty($option[0]['value'])) {
+      // Get value from default options.
+      // @see \Drupal\webform\Plugin\WebformElement\WebformTableTrait::prepare
+      return $option[0]['value'];
+    }
+    else {
+      return NULL;
     }
-    return [$title => $selectors];
   }
 
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformTermReferenceTrait.php b/web/modules/webform/src/Plugin/WebformElement/WebformTermReferenceTrait.php
index 6e48da43e538ec9c80e533c2fbb7769b06f13ec8..3382c47ee37d26be6d23d902fae7825c028133c1 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformTermReferenceTrait.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformTermReferenceTrait.php
@@ -32,6 +32,16 @@ public function preview() {
       $vocabulary_id = 'tags';
     }
 
+    // Make sure the vocabulary does not have more than 250 terms.
+    // This will prevent a fatal memory error when
+    // previewing term related elements.
+    /** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */
+    $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
+    $tree = $taxonomy_storage->loadTree($vocabulary_id);
+    if (count($tree) > 250) {
+      $vocabulary_id = NULL;
+    }
+
     return parent::preview() + [
       '#vocabulary' => $vocabulary_id,
     ];
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformTime.php b/web/modules/webform/src/Plugin/WebformElement/WebformTime.php
index 6b0159fe4565160d596bb9804e7aca00990f21b8..4f3b4923ab2040a604ac9ce5aa90e7c10430a6e9 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformTime.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformTime.php
@@ -59,7 +59,7 @@ protected function formatTextItem(array $element, WebformSubmissionInterface $we
     $format = $this->getItemFormat($element);
     if ($format == 'value') {
       $time_format = (isset($element['#time_format'])) ? $element['#time_format'] : 'H:i';
-      return date($time_format, strtotime($value));
+      return static::formatTime($time_format, strtotime($value));
     }
 
     return parent::formatTextItem($element, $webform_submission, $options);
@@ -90,24 +90,24 @@ public function form(array $form, FormStateInterface $form_state) {
       '#type' => 'webform_select_other',
       '#title' => $this->t('Time format'),
       '#options' => [
-        'H:i' => $this->t('24 hour - @format (@time)', ['@format' => 'H:i', '@time' => date('H:i')]),
-        'H:i:s' => $this->t('24 hour with seconds - @format (@time)', ['@format' => 'H:i:s', '@time' => date('H:i:s')]),
-        'g:i A' => $this->t('12 hour - @format (@time)', ['@format' => 'g:i A', '@time' => date('g:i A')]),
-        'g:i:s A' => $this->t('12 hour with seconds - @format (@time)', ['@format' => 'g:i:s A', '@time' => date('g:i:s A')]),
+        'H:i' => $this->t('24 hour - @format (@time)', ['@format' => 'H:i', '@time' => static::formatTime('H:i')]),
+        'H:i:s' => $this->t('24 hour with seconds - @format (@time)', ['@format' => 'H:i:s', '@time' => static::formatTime('H:i:s')]),
+        'g:i A' => $this->t('12 hour - @format (@time)', ['@format' => 'g:i A', '@time' => static::formatTime('g:i A')]),
+        'g:i:s A' => $this->t('12 hour with seconds - @format (@time)', ['@format' => 'g:i:s A', '@time' => static::formatTime('g:i:s A')]),
       ],
-      '#other__option_label' => $this->t('Custom...'),
-      '#other__placeholder' => $this->t('Custom time format...'),
+      '#other__option_label' => $this->t('Custom…'),
+      '#other__placeholder' => $this->t('Custom time format…'),
       '#other__description' => $this->t('Enter time format using <a href="http://php.net/manual/en/function.date.php">Time Input Format</a>.'),
     ];
     $form['time']['time_container'] = $this->getFormInlineContainer();
     $form['time']['time_container']['min'] = [
       '#type' => 'webform_time',
-      '#title' => $this->t('Min'),
+      '#title' => $this->t('Minimum'),
       '#description' => $this->t('Specifies the minimum time.'),
     ];
     $form['time']['time_container']['max'] = [
       '#type' => 'webform_time',
-      '#title' => $this->t('Max'),
+      '#title' => $this->t('Maximum'),
       '#description' => $this->t('Specifies the maximum time.'),
     ];
     $form['time']['step'] = [
@@ -128,4 +128,21 @@ public function form(array $form, FormStateInterface $form_state) {
     return $form;
   }
 
+  /**
+   * Format custom time.
+   *
+   * @param string $custom_format
+   *   A PHP date format string suitable for input to date().
+   * @param int $timestamp
+   *   (optional) A UNIX timestamp to format.
+   *
+   * @return string
+   *   Formatted time.
+   */
+  protected static function formatTime($custom_format, $timestamp = NULL) {
+    /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
+    $date_formatter = \Drupal::service('date.formatter');
+    return $date_formatter->format($timestamp ?: time(), 'custom', $custom_format);
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformToggle.php b/web/modules/webform/src/Plugin/WebformElement/WebformToggle.php
index 88f06fbcf1f1868a46330463b363f7ad3f11d3fa..2565a02fdf812e98bebec3095a0e9558dbe1f210 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformToggle.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformToggle.php
@@ -12,6 +12,8 @@
  *   label = @Translation("Toggle"),
  *   description = @Translation("Provides a form element for toggling a single on/off state."),
  *   category = @Translation("Advanced elements"),
+ *   deprecated = TRUE,
+ *   deprecated_message = @Translation("The Toogles library is not being maintained and has major accessibility issues. It has been <a href=""https://www.drupal.org/project/webform/issues/2890861"">deprecated</a> and will be removed before Webform 8.x-5.0."),
  * )
  */
 class WebformToggle extends Checkbox {
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformToggleTrait.php b/web/modules/webform/src/Plugin/WebformElement/WebformToggleTrait.php
index 62dc041d99b19a5d0b29896ca43e3570c7fa2218..b6e63e6159976d333f6eebe2883c1f058cccee74 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformToggleTrait.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformToggleTrait.php
@@ -9,6 +9,17 @@
  */
 trait WebformToggleTrait {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isExcluded() {
+    if (\Drupal::service('webform.libraries_manager')->isExcluded('jquery.toggles')) {
+      return TRUE;
+    }
+
+    return parent::isExcluded();
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformToggles.php b/web/modules/webform/src/Plugin/WebformElement/WebformToggles.php
index 44950c177b2cad197dfc0d2724e1cd43b5f80973..8c8839bc8cb4c362a8f4a0d1ed43cccecf92132e 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformToggles.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformToggles.php
@@ -2,8 +2,6 @@
 
 namespace Drupal\webform\Plugin\WebformElement;
 
-use Drupal\webform\WebformSubmissionInterface;
-
 /**
  * Provides a 'toggles' element.
  *
@@ -12,6 +10,8 @@
  *   label = @Translation("Toggles"),
  *   description = @Translation("Provides a form element for toggling multiple on/off states."),
  *   category = @Translation("Options elements"),
+ *   deprecated = TRUE,
+ *   deprecated_message = @Translation("The Toogles library is not being maintained and has major accessibility issues. It has been <a href=""https://www.drupal.org/project/webform/issues/2890861"">deprecated</a> and will be removed before Webform 8.x-5.0."),
  * )
  */
 class WebformToggles extends OptionsBase {
@@ -47,14 +47,6 @@ public function hasMultipleValues(array $element) {
     return TRUE;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
-    $element['#element_validate'][] = [get_class($this), 'validateMultipleOptions'];
-    parent::prepare($element, $webform_submission);
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/web/modules/webform/src/Plugin/WebformElement/WebformWizardPage.php b/web/modules/webform/src/Plugin/WebformElement/WebformWizardPage.php
index 89aacbb35392288056c5ca4d62b79b07442f66da..f65f22086ff637b2074225c92edb5b9c1313c7e4 100644
--- a/web/modules/webform/src/Plugin/WebformElement/WebformWizardPage.php
+++ b/web/modules/webform/src/Plugin/WebformElement/WebformWizardPage.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\webform\WebformInterface;
+use Drupal\webform\WebformSubmissionInterface;
 
 /**
  * Provides a 'webform_wizard_page' element.
@@ -11,7 +12,7 @@
  * @WebformElement(
  *   id = "webform_wizard_page",
  *   label = @Translation("Wizard page"),
- *   description = @Translation("Provides an element to display multiple form elements as a page in a multistep form wizard."),
+ *   description = @Translation("Provides an element to display multiple form elements as a page in a multi-step form wizard."),
  *   category = @Translation("Wizard"),
  * )
  */
@@ -66,6 +67,27 @@ public function preview() {
     return [];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function formatHtmlItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
+    $build = parent::formatHtmlItem($element, $webform_submission, $options);
+
+    // Add edit page link container to preview.
+    // @see Drupal.behaviors.webformWizardPagesLink
+    if ($build && isset($options['view_mode']) && $options['view_mode'] === 'preview' && $webform_submission->getWebform()->getSetting('wizard_preview_link')) {
+      $build['#children']['wizard_page_link'] = [
+        '#type' => 'container',
+        '#attributes' => [
+          'data-webform-page' => $element['#webform_key'],
+          'class' => ['webform-wizard-page-edit'],
+        ],
+      ];
+    }
+
+    return $build;
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/web/modules/webform/src/Plugin/WebformElementAttachmentInterface.php b/web/modules/webform/src/Plugin/WebformElementAttachmentInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..dfca60e1fe83ca3eaeaf4ff470787ff4c74ac3cd
--- /dev/null
+++ b/web/modules/webform/src/Plugin/WebformElementAttachmentInterface.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\webform\Plugin;
+
+use Drupal\webform\WebformSubmissionInterface;
+
+/**
+ * Defines the interface for webform elements can provide email attachments.
+ */
+interface WebformElementAttachmentInterface {
+
+  /**
+   * Get email attachments.
+   *
+   * @param array $element
+   *   An element.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   * @param array $options
+   *   An array of options.
+   *
+   * @return array
+   *   An array containing email attachments which include an attachments
+   *   'filename', 'filemime', 'filepath', and 'filecontent'.
+   *
+   * @see \Drupal\mimemail\Utility\MimeMailFormatHelper::mimeMailHtmlBody
+   * @see \Drupal\smtp\Plugin\Mail\SMTPMailSystem::mail
+   * @see \Drupal\swiftmailer\Plugin\Mail\SwiftMailer::attachAsMimeMail
+   */
+  public function getAttachments(array $element, WebformSubmissionInterface $webform_submission, array $options = []);
+
+}
diff --git a/web/modules/webform/src/Plugin/WebformElementBase.php b/web/modules/webform/src/Plugin/WebformElementBase.php
index 6b4c0f01832aacb41172b6dd31c33107fd1f2f71..ca66dbecfb6ceb8af359e6e18614aaa874d384d0 100644
--- a/web/modules/webform/src/Plugin/WebformElementBase.php
+++ b/web/modules/webform/src/Plugin/WebformElementBase.php
@@ -4,25 +4,29 @@
 
 use Drupal\Component\Plugin\PluginBase;
 use Drupal\Component\Utility\NestedArray;
-use Drupal\Component\Utility\Unicode;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Form\OptGroup;
 use Drupal\Core\Link;
+use Drupal\Core\Messenger\MessengerTrait;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\ElementInfoManagerInterface;
 use Drupal\Core\Render\Markup;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Url;
+use Drupal\webform\Element\WebformCompositeFormElementTrait;
 use Drupal\webform\Element\WebformHtmlEditor;
 use Drupal\webform\Element\WebformMessage;
 use Drupal\webform\Entity\WebformOptions;
 use Drupal\webform\Plugin\WebformElement\Checkbox;
 use Drupal\webform\Plugin\WebformElement\Checkboxes;
+use Drupal\webform\Plugin\WebformElement\ContainerBase;
 use Drupal\webform\Plugin\WebformElement\Details;
+use Drupal\webform\Plugin\WebformElement\WebformCompositeBase;
 use Drupal\webform\Twig\TwigExtension;
 use Drupal\webform\Utility\WebformArrayHelper;
 use Drupal\webform\Utility\WebformElementHelper;
@@ -49,6 +53,8 @@
 class WebformElementBase extends PluginBase implements WebformElementInterface {
 
   use StringTranslationTrait;
+  use MessengerTrait;
+  use WebformCompositeFormElementTrait;
 
   /**
    * A logger instance.
@@ -192,6 +198,7 @@ public function getDefaultProperties() {
       'default_value' => '',
       // Description/Help.
       'help' => '',
+      'help_title' => '',
       'description' => '',
       'more' => '',
       'more_title' => '',
@@ -204,12 +211,9 @@ public function getDefaultProperties() {
       // Form validation.
       'required' => FALSE,
       'required_error' => '',
-      'unique' => FALSE,
-      'unique_user' => FALSE,
-      'unique_entity' => FALSE,
-      'unique_error' => '',
       // Attributes.
       'wrapper_attributes' => [],
+      'label_attributes' => [],
       'attributes' => [],
       // Submission display.
       'format' => $this->getItemDefaultFormat(),
@@ -218,8 +222,19 @@ public function getDefaultProperties() {
       'format_items' => $this->getItemsDefaultFormat(),
       'format_items_html' => '',
       'format_items_text' => '',
+      'format_attributes' => [],
     ];
 
+    // Unique validation.
+    if (!$this->isComposite()) {
+      $properties += [
+        'unique' => FALSE,
+        'unique_user' => FALSE,
+        'unique_entity' => FALSE,
+        'unique_error' => '',
+      ];
+    }
+
     $properties += $this->getDefaultBaseProperties();
 
     return $properties;
@@ -235,11 +250,13 @@ protected function getDefaultMultipleProperties() {
     return [
       'multiple' => FALSE,
       'multiple__header_label' => '',
-      'multiple__label' => $this->t('item'),
-      'multiple__labels' => $this->t('items'),
-      'multiple__min_items' => 1,
+      'multiple__min_items' => '',
       'multiple__empty_items' => 1,
-      'multiple__add_more' => 1,
+      'multiple__add_more' => TRUE,
+      'multiple__add_more_items' => 1,
+      'multiple__add_more_button_label' => (string) $this->t('Add'),
+      'multiple__add_more_input_label' => (string) $this->t('more items'),
+      'multiple__no_items_message' => (string) $this->t('No items entered. Please add items below.'),
       'multiple__sorting' => TRUE,
       'multiple__operations' => TRUE,
     ];
@@ -261,6 +278,7 @@ protected function getDefaultBaseProperties() {
       'flex' => 1,
       // Conditional logic.
       'states' => [],
+      'states_clear' => TRUE,
       // Element access.
       'access_create_roles' => ['anonymous', 'authenticated'],
       'access_create_users' => [],
@@ -282,6 +300,7 @@ public function getTranslatableProperties() {
       'title',
       'label',
       'help',
+      'help_title',
       'more',
       'more_title',
       'description',
@@ -294,6 +313,10 @@ public function getTranslatableProperties() {
       'markup',
       'test',
       'default_value',
+      'header_label',
+      'add_more_button_label',
+      'add_more_input_label',
+      'no_items_message',
     ];
   }
 
@@ -360,7 +383,7 @@ protected function getElementInfoDefaultProperty(array $element, $property_name)
     }
     $property_name = ($property_name[0] !== '#') ? '#' . $property_name : $property_name;
     $type = $element['#type'];
-    return $property_value = $this->elementInfo->getInfoProperty($type, $property_name, NULL)
+    return $this->elementInfo->getInfoProperty($type, $property_name, NULL)
       ?: $this->elementInfo->getInfoProperty("webform_$type", $property_name, NULL);
   }
 
@@ -410,6 +433,13 @@ public function getPluginDescription() {
     return $this->pluginDefinition['description'];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginCategory() {
+    return $this->pluginDefinition['category'] ?: $this->t('Other elements');
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -452,6 +482,13 @@ public function isRoot() {
     return FALSE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function hasManagedFiles(array $element) {
+    return FALSE;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -575,8 +612,8 @@ public function getRelatedTypes(array $element) {
         continue;
       }
 
-      // Skip disable or hidden.
-      if (!$element_instance->isEnabled() || $element_instance->isHidden()) {
+      // Skip disabled or hidden.
+      if ($element_instance->isDisabled() || $element_instance->isHidden()) {
         continue;
       }
 
@@ -621,9 +658,6 @@ public function initialize(array &$element) {
     if (!empty($element['#title']) && empty($element['#admin_title'])) {
       $element['#admin_title'] = strip_tags($element['#title']);
     }
-
-    // Replace global tokens which could include the [site:name].
-    $this->replaceTokens($element);
   }
 
   /**
@@ -649,6 +683,10 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       $element['#access'] = $this->checkAccessRules($operation, $element);
     }
 
+    // Enable webform template preprocessing enhancements.
+    // @see \Drupal\webform\Utility\WebformElementHelper::isWebformElement
+    $element['#webform_element'] = TRUE;
+
     // Add #allowed_tags.
     $allowed_tags = $this->configFactory->get('webform.settings')->get('element.allowed_tags');
     switch ($allowed_tags) {
@@ -680,9 +718,17 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       $element['#wrapper_attributes']['class'][] = 'webform-element--title-inline';
     }
 
-    // Check description markup.
-    if (isset($element['#description'])) {
-      $element['#description'] = WebformHtmlEditor::checkMarkup($element['#description']);
+    // Check markup properties.
+    $markup_properties = [
+      '#description',
+      '#help',
+      '#more',
+      '#multiple__no_items_message',
+    ];
+    foreach ($markup_properties as $markup_property) {
+      if (isset($element[$markup_property])) {
+        $element[$markup_property] = WebformHtmlEditor::checkMarkup($element[$markup_property]);
+      }
     }
 
     // Add default description display.
@@ -697,8 +743,6 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       $element[$attributes_property]['class'][] = 'js-webform-tooltip-element';
       $element[$attributes_property]['class'][] = 'webform-tooltip-element';
       $element['#attached']['library'][] = 'webform/webform.tooltip';
-      // More is not supported with tooltip.
-      unset($element['#more']);
     }
 
     // Add iCheck support.
@@ -730,9 +774,15 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       $element[$attributes_property]['class'][] = 'webform-has-field-suffix';
     }
 
-    // Set element's #element_validate callback so that is not replaced by
-    // additional #element_validate callbacks.
+    // Add 'data-webform-states-no-clear' attribute if #states_clear is FALSE.
+    if (isset($element['#states_clear']) && $element['#states_clear'] === FALSE) {
+      $element[$attributes_property]['data-webform-states-no-clear'] = TRUE;
+    }
+
+    // Set element's #element_validate callback so that is not replaced when
+    // we append additional #element_validate callbacks.
     $this->setElementDefaultCallback($element, 'element_validate');
+    $this->prepareElementValidateCallbacks($element, $webform_submission);
 
     if ($this->isInput($element)) {
       // Handle #readonly support.
@@ -750,18 +800,6 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
       if (!empty($element['#required_error'])) {
         $element['#attributes']['data-webform-required-error'] = $element['#required_error'];
       }
-
-      // Add webform element #minlength, #unique, and/or #multiple
-      // validation handler.
-      if (isset($element['#minlength'])) {
-        $element['#element_validate'][] = [get_class($this), 'validateMinlength'];
-      }
-      if (isset($element['#unique'])) {
-        $element['#element_validate'][] = [get_class($this), 'validateUnique'];
-      }
-      if (isset($element['#multiple']) && $element['#multiple'] > 1) {
-        $element['#element_validate'][] = [get_class($this), 'validateMultiple'];
-      }
     }
 
     // Replace tokens for all properties.
@@ -774,6 +812,14 @@ public function prepare(array &$element, WebformSubmissionInterface $webform_sub
    * {@inheritdoc}
    */
   public function finalize(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    // Set element's #element_validate callback so that is not replaced when
+    // we append additional #pre_render callbacks.
+    $this->setElementDefaultCallback($element, 'pre_render');
+    $this->prepareElementPreRenderCallbacks($element, $webform_submission);
+
+    // Prepare composite element.
+    $this->prepareCompositeFormElement($element);
+
     // Prepare multiple element.
     $this->prepareMultipleWrapper($element);
 
@@ -865,10 +911,80 @@ protected function checkAccessRule(array $element, $operation, AccountInterface
   /**
    * {@inheritdoc}
    */
-  public function replaceTokens(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+  public function replaceTokens(array &$element, EntityInterface $entity = NULL) {
     foreach ($element as $key => $value) {
-      if (!Element::child($key)) {
-        $element[$key] = $this->tokenManager->replace($value, $webform_submission);
+      // Only replace tokens in properties.
+      if (Element::child($key)) {
+        continue;
+      }
+
+      // Ignore tokens in #template and #format_* properties.
+      if (in_array($key, ['#template', '#format_html', '#format_text', 'format_items_html', 'format_items_text'])) {
+        continue;
+      }
+
+      $element[$key] = $this->tokenManager->replace($value, $entity);
+    }
+  }
+
+  /**
+   * Prepare an element's validation callbacks.
+   *
+   * @param array $element
+   *   An element.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   */
+  protected function prepareElementValidateCallbacks(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    // Validation callbacks are only applicable to inputs.
+    if (!$this->isInput($element)) {
+      return;
+    }
+
+    // Add webform element #minlength, #multiple, and/or #unique
+    // validation handler.
+    if (isset($element['#minlength'])) {
+      $element['#element_validate'][] = [get_class($this), 'validateMinlength'];
+    }
+    if (isset($element['#multiple']) && $element['#multiple'] > 1) {
+      $element['#element_validate'][] = [get_class($this), 'validateMultiple'];
+    }
+    if (isset($element['#unique']) && $webform_submission) {
+      $element['#element_validate'][] = [get_class($this), 'validateUnique'];
+    }
+  }
+
+  /**
+   * Prepare an element's pre render callbacks.
+   *
+   * @param array $element
+   *   An element.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   */
+  protected function prepareElementPreRenderCallbacks(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
+    // Do nothing.
+  }
+
+  /**
+   * Replace Core's composite #pre_render with Webform's composite #pre_render.
+   *
+   * @param array $element
+   *   An element.
+   *
+   * @see \Drupal\Core\Render\Element\CompositeFormElementTrait
+   * @see \Drupal\webform\Element\WebformCompositeFormElementTrait
+   */
+  protected function prepareCompositeFormElement(array &$element) {
+    if (empty($element['#pre_render'])) {
+      return;
+    }
+
+    // Replace preRenderCompositeFormElement with
+    // preRenderWebformCompositeFormElement.
+    foreach ($element['#pre_render'] as $index => $pre_render) {
+      if (is_array($pre_render) && $pre_render[1] === 'preRenderCompositeFormElement') {
+        $element['#pre_render'][$index] = [get_called_class(), 'preRenderWebformCompositeFormElement'];
       }
     }
   }
@@ -888,10 +1004,6 @@ protected function prepareWrapper(array &$element) {
 
     $class = get_class($this);
 
-    // Set element's #pre_render callback so that is not replaced by
-    // additional element #pre_render callbacks.
-    $this->setElementDefaultCallback($element, 'pre_render');
-
     // Fix #states wrapper.
     if ($has_states_wrapper) {
       $element['#pre_render'][] = [$class, 'preRenderFixStatesWrapper'];
@@ -954,16 +1066,26 @@ protected function prepareMultipleWrapper(array &$element) {
 
     // Set the multiple element.
     $element['#element'] = $element;
+
     // Remove properties that should only be applied to the parent element.
-    $element['#element'] = array_diff_key($element['#element'], array_flip(['#default_value', '#description', '#description_display', '#required', '#required_error', '#states', '#wrapper_attributes', '#prefix', '#suffix', '#element', '#tags', '#multiple']));
+    $element['#element'] = array_diff_key($element['#element'], array_flip(['#access', '#default_value', '#description', '#description_display', '#required', '#required_error', '#states', '#wrapper_attributes', '#prefix', '#suffix', '#element', '#tags', '#multiple']));
+
     // Propagate #states to sub element.
     // @see \Drupal\webform\Element\WebformCompositeBase::processWebformComposite
     if (!empty($element['#states'])) {
       $element['#element']['#_webform_states'] = $element['#states'];
     }
+
     // Always make the title invisible.
     $element['#element']['#title_display'] = 'invisible';
 
+    // Set hidden element #after_build handler.
+    $element['#element']['#after_build'][] = [get_class($this), 'hiddenElementAfterBuild'];
+
+    // Remove 'for' from the main element's label.
+    // This must be done after the $element['#element' is defined.
+    $element['#label_attributes']['webform-remove-for-attribute'] = TRUE;
+
     // Change the element to a multiple element.
     $element['#type'] = 'webform_multiple';
     $element['#webform_multiple'] = TRUE;
@@ -1011,7 +1133,7 @@ public function displayDisabledWarning(array $element) {
     else {
       $message = $this->t('%title is a %type element, which has been disabled and will not be rendered. Please contact a site administrator.', $t_args);
     }
-    drupal_set_message($message, 'warning');
+    $this->messenger()->addWarning($message);
 
     $context = [
       '@title' => $this->getLabel($element),
@@ -1030,13 +1152,14 @@ public function setDefaultValue(array &$element) {}
    * {@inheritdoc}
    */
   public function getLabel(array $element) {
-    return $element['#title'] ?: $element['#webform_key'];
+    return (!empty($element['#title'])) ? $element['#title'] : $element['#webform_key'];
   }
 
   /**
    * {@inheritdoc}
    */
   public function getAdminLabel(array $element) {
+    $element += ['#admin_title' => '', '#title' => '', '#webform_key' => ''];
     return $element['#admin_title'] ?: $element['#title'] ?: $element['#webform_key'];
   }
 
@@ -1081,17 +1204,14 @@ public function buildText(array $element, WebformSubmissionInterface $webform_su
    *   A render array representing an element as text or HTML.
    */
   protected function build($format, array &$element, WebformSubmissionInterface $webform_submission, array $options = []) {
-    $options += [
-      'exclude_empty' => TRUE,
-    ];
     $options['multiline'] = $this->isMultiline($element);
     $format_function = 'format' . ucfirst($format);
     $value = $this->$format_function($element, $webform_submission, $options);
 
     // Handle empty value.
     if ($value === '') {
-      // Return NULL for empty formatted value.
-      if (!empty($options['exclude_empty'])) {
+      // Return NULL if empty is excluded.
+      if ($this->isEmptyExcluded($element, $options)) {
         return NULL;
       }
       // Else set the formatted value to empty message/placeholder.
@@ -1156,7 +1276,7 @@ protected function format($type, array &$element, WebformSubmissionInterface $we
       if (isset($options['delta'])) {
         return $this->$item_function($element, $webform_submission, $options);
       }
-      elseif ($this->getItemsFormat($element) == 'custom') {
+      elseif ($this->getItemsFormat($element) === 'custom' && !empty($element['#format_items_' . strtolower($type)])) {
         return $this->formatCustomItems($type, $element, $webform_submission, $options);
       }
       else {
@@ -1164,7 +1284,7 @@ protected function format($type, array &$element, WebformSubmissionInterface $we
       }
     }
     else {
-      if ($this->getItemFormat($element) == 'custom') {
+      if ($this->getItemFormat($element) === 'custom' && !empty($element['#format_' . strtolower($type)])) {
         return $this->formatCustomItem($type, $element, $webform_submission, $options);
       }
       else {
@@ -1184,11 +1304,13 @@ protected function format($type, array &$element, WebformSubmissionInterface $we
    *   A webform submission.
    * @param array $options
    *   An array of options.
+   * @param array $context
+   *   (optional) Context to be passed to inline Twig template.
    *
    * @return array|string
    *   The element's items formatted as plain text or a render array.
    */
-  protected function formatCustomItems($type, array &$element, WebformSubmissionInterface $webform_submission, array $options = []) {
+  protected function formatCustomItems($type, array &$element, WebformSubmissionInterface $webform_submission, array $options = [], array $context = []) {
     $name = strtolower($type);
 
     // Get value.
@@ -1205,18 +1327,13 @@ protected function formatCustomItems($type, array &$element, WebformSubmissionIn
     $template = trim($element['#format_items_' . $name]);
 
     // Get context.
-    $context = (isset($options['context'])) ? $options['context'] : [];
+    $options += ['context' => []];
     $context += [
       'value' => $value,
       'items' => $items,
-      'data' => $webform_submission->getData(),
     ];
 
-    return [
-      '#type' => 'inline_template',
-      '#template' => $template,
-      '#context' => $context,
-    ];
+    return TwigExtension::buildTwigTemplate($webform_submission, $template, $options, $context);
   }
 
   /**
@@ -1375,22 +1492,20 @@ protected function formatTextItems(array &$element, WebformSubmissionInterface $
    *   A webform submission.
    * @param array $options
    *   An array of options.
+   * @param array $context
+   *   (optional) Context to be passed to inline Twig template.
    *
    * @return array|string
    *   The element's item formatted as plain text or a render array.
    */
-  protected function formatCustomItem($type, array &$element, WebformSubmissionInterface $webform_submission, array $options = []) {
+  protected function formatCustomItem($type, array &$element, WebformSubmissionInterface $webform_submission, array $options = [], array $context = []) {
     $name = strtolower($type);
 
     // Get template.
     $template = trim($element['#format_' . $name]);
 
-    // Get context.
-    $context = (isset($options['context'])) ? $options['context'] : [];
-
     // Get context.value.
-    $value = $this->getValue($element, $webform_submission, $options);
-    $context['value'] = $value;
+    $context['value'] = $this->getValue($element, $webform_submission, $options);
 
     // Get content.item.
     $context['item'] = [];
@@ -1403,15 +1518,13 @@ protected function formatCustomItem($type, array &$element, WebformSubmissionInt
       }
     }
 
-    // Add submission data to context.
-    $context['data'] = $webform_submission->getData();
-
     // Return inline template.
-    return [
-      '#type' => 'inline_template',
-      '#template' => $template,
-      '#context' => $context,
-    ];
+    if ($type === 'Text') {
+      return TwigExtension::renderTwigTemplate($webform_submission, $template, $options, $context);
+    }
+    else {
+      return TwigExtension::buildTwigTemplate($webform_submission, $template, $options, $context);
+    }
   }
 
   /**
@@ -1437,15 +1550,7 @@ protected function formatHtmlItem(array $element, WebformSubmissionInterface $we
 
     // Build a render that used #plain_text so that HTML characters are escaped.
     // @see \Drupal\Core\Render\Renderer::ensureMarkupIsSafe
-    if ($value === '0') {
-      // Issue #2765609: #plain_text doesn't render empty-like values
-      // (e.g. 0 and "0").
-      // Workaround: Use #markup until this issue is fixed.
-      $build = ['#markup' => $value];
-    }
-    else {
-      $build = ['#plain_text' => $value];
-    }
+    $build = ['#plain_text' => $value];
 
     $options += ['prefixing' => TRUE];
     if ($options['prefixing']) {
@@ -1665,6 +1770,16 @@ public function formatTableColumn(array $element, WebformSubmissionInterface $we
     return $this->formatHtml($element, $webform_submission);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmptyExcluded(array $element, array $options) {
+    $options += [
+      'exclude_empty' => TRUE,
+    ];
+    return !empty($options['exclude_empty']);
+  }
+
   /****************************************************************************/
   // Export methods.
   /****************************************************************************/
@@ -1745,11 +1860,11 @@ public static function validateMinlength(&$element, FormStateInterface &$form_st
       return;
     }
 
-    if (Unicode::strlen($element['#value']) < $element['#minlength']) {
+    if (!empty($element['#value']) && mb_strlen($element['#value']) < $element['#minlength']) {
       $t_args = [
         '%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title'],
         '%min' => $element['#minlength'],
-        '%length' => Unicode::strlen($element['#value']),
+        '%length' => mb_strlen($element['#value']),
       ];
       $form_state->setError($element, t('%name cannot be less than %min characters but is currently %length characters long.', $t_args));
     }
@@ -1763,11 +1878,20 @@ public static function validateUnique(array &$element, FormStateInterface $form_
       return;
     }
 
+    /** @var \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager */
+    $element_manager = \Drupal::service('plugin.manager.webform.element');
+
     $name = $element['#webform_key'];
     $value = NestedArray::getValue($form_state->getValues(), $element['#parents']);
 
-    // Skip empty unique fields or composite arrays.
-    if ($value === '' || is_array($value)) {
+    // Skip composite elements.
+    $element_plugin = $element_manager->getElementInstance($element);
+    if ($element_plugin->isComposite()) {
+      return;
+    }
+
+    // Skip empty values but allow for '0'.
+    if ($value === '' || $value === NULL || (is_array($value) && empty($value))) {
       return;
     }
 
@@ -1777,13 +1901,13 @@ public static function validateUnique(array &$element, FormStateInterface $form_
     $webform_submission = $form_object->getEntity();
     $webform = $webform_submission->getWebform();
 
-    // Build unique query.
+    // Build unique query which return a single duplicate value.
     $query = \Drupal::database()->select('webform_submission', 'ws');
     $query->leftJoin('webform_submission_data', 'wsd', 'ws.sid = wsd.sid');
-    $query->fields('ws', ['sid']);
+    $query->fields('wsd', ['value']);
     $query->condition('wsd.webform_id', $webform->id());
     $query->condition('wsd.name', $name);
-    $query->condition('wsd.value', $value);
+    $query->condition('wsd.value', (array) $value, 'IN');
     // Unique user condition.
     if (!empty($element['#unique_user'])) {
       $query->condition('ws.uid', $webform_submission->getOwnerId());
@@ -1803,24 +1927,31 @@ public static function validateUnique(array &$element, FormStateInterface $form_
     if ($sid = $webform_submission->id()) {
       $query->condition('ws.sid', $sid, '<>');
     }
-    // Using range() is more efficient than using countQuery() for data checks.
+    // Get single duplicate value.
     $query->range(0, 1);
-    $count = $query->execute()->fetchField();
+    $duplicate_value = $query->execute()->fetchField();
 
-    if ($count) {
-      if (isset($element['#unique_error'])) {
-        $form_state->setError($element, $element['#unique_error']);
-      }
-      elseif (isset($element['#title'])) {
-        $t_args = [
-          '%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title'],
-          '%value' => $value,
-        ];
-        $form_state->setError($element, t('The value %value has already been submitted once for the %name element. You may have already submitted this webform, or you need to use a different value.', $t_args));
-      }
-      else {
-        $form_state->setError($element);
+    // Skip NULL or empty string value.
+    if ($duplicate_value === FALSE || $duplicate_value === '') {
+      return;
+    }
+
+    if (isset($element['#unique_error'])) {
+      $form_state->setError($element, $element['#unique_error']);
+    }
+    elseif (isset($element['#title'])) {
+      // Get #options display value.
+      if (isset($element['#options'])) {
+        $duplicate_value = WebformOptionsHelper::getOptionText($duplicate_value, $element['#options'], TRUE);
       }
+      $t_args = [
+        '%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title'],
+        '%value' => $duplicate_value,
+      ];
+      $form_state->setError($element, t('The value %value has already been submitted once for the %name element. You may have already submitted this webform, or you need to use a different value.', $t_args));
+    }
+    else {
+      $form_state->setError($element);
     }
   }
 
@@ -1925,7 +2056,7 @@ public function getElementStateOptions() {
       $validation_optgroup => [
         'required' => $this->t('Required'),
         'optional' => $this->t('Optional'),
-      ]
+      ],
     ];
 
     // Set readwrite/readonly states for any element that supports it
@@ -1958,8 +2089,6 @@ public function getElementStateOptions() {
 
   /**
    * {@inheritdoc}
-   *
-   * @see \Drupal\webform\Entity\Webform::getElementsSelectorOptions
    */
   public function getElementSelectorOptions(array $element) {
     if ($this->hasMultipleValues($element) && $this->hasMultipleWrapper()) {
@@ -1981,6 +2110,13 @@ public function getElementSelectorOptions(array $element) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getElementSelectorSourceValues(array $element) {
+    return [];
+  }
+
   /**
    * Get an element's (sub)inputs selectors as options.
    *
@@ -2107,49 +2243,53 @@ public function form(array $form, FormStateInterface $form_state) {
       ],
     ];
 
-    /* Element description */
+    /* Element description/help/more */
 
     $form['element_description'] = [
       '#type' => 'details',
-      '#title' => $this->t('Element description/help'),
+      '#title' => $this->t('Element description/help/more'),
     ];
-    $form['element_description']['help'] = [
+    $form['element_description']['description'] = [
       '#type' => 'webform_html_editor',
-      '#title' => $this->t('Help text'),
-      '#description' => $this->t('A tooltip displayed after the title.'),
+      '#title' => $this->t('Description'),
+      '#description' => $this->t('A short description of the element used as help for the user when he/she uses the webform.'),
+    ];
+    $form['element_description']['help'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Help'),
+      '#description' => $this->t("Displays a help tooltip after the element's title."),
       '#states' => [
         'invisible' => [
           [':input[name="properties[title_display]"]' => ['value' => 'invisible']],
-          'or',
-          [':input[name="properties[title_display]"]' => ['value' => 'attribute']],
         ],
       ],
     ];
-    $form['element_description']['description'] = [
+    $form['element_description']['help']['help_title'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Help title'),
+      '#description' => $this->t("The text displayed in help tooltip after the element's title.") . '<br /><br />' .
+        $this->t("Defaults to the element's title"),
+    ];
+    $form['element_description']['help']['help'] = [
       '#type' => 'webform_html_editor',
-      '#title' => $this->t('Description'),
-      '#description' => $this->t('A short description of the element used as help for the user when he/she uses the webform.'),
+      '#title' => $this->t('Help text'),
+      '#description' => $this->t("The text displayed in help tooltip after the element's title."),
     ];
-    $form['element_description']['more_title'] = [
+    $form['element_description']['more'] = [
+      '#type' => 'details',
+      '#title' => $this->t('More'),
+      '#description' => $this->t("Displays a read more hide/show widget below the element's description."),
+    ];
+    $form['element_description']['more']['more_title'] = [
       '#type' => 'textfield',
-      '#title' => $this->t('More label'),
+      '#title' => $this->t('More title'),
       '#description' => $this->t('The click-able label used to open and close more text.') . '<br /><br />' .
         $this->t('Defaults to: %value', ['%value' => $this->configFactory->get('webform.settings')->get('element.default_more_title')]),
-      '#states' => [
-        'invisible' => [
-          ':input[name="properties[description_display]"]' => ['value' => 'tooltip'],
-        ],
-      ],
     ];
-    $form['element_description']['more'] = [
+    $form['element_description']['more']['more'] = [
       '#type' => 'webform_html_editor',
       '#title' => $this->t('More text'),
       '#description' => $this->t('A long description of the element that provides form additional information which can opened and closed.'),
-      '#states' => [
-        'invisible' => [
-          ':input[name="properties[description_display]"]' => ['value' => 'tooltip'],
-        ],
-      ],
     ];
 
     /* Form display */
@@ -2168,7 +2308,6 @@ public function form(array $form, FormStateInterface $form_state) {
         'after' => $this->t('After'),
         'inline' => $this->t('Inline'),
         'invisible' => $this->t('Invisible'),
-        'attribute' => $this->t('Attribute'),
       ],
       '#description' => $this->t('Determines the placement of the title.'),
     ];
@@ -2184,6 +2323,13 @@ public function form(array $form, FormStateInterface $form_state) {
       ],
       '#description' => $this->t('Determines the placement of the description.'),
     ];
+
+    // Remove unsupported title and description display from composite elements.
+    if ($this->isComposite()) {
+      unset($form['form']['display_container']['title_display']['#options']['inline']);
+      unset($form['form']['display_container']['description_display']['#options']['tooltip']);
+    }
+
     $form['form']['field_container'] = $this->getFormInlineContainer();
     $form['form']['field_container']['field_prefix'] = [
       '#type' => 'textfield',
@@ -2243,14 +2389,14 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['form']['disabled'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Disabled'),
-      '#description' => $this->t('Make this element non-editable with the value <strong>ignored</strong>. Useful for displaying default value. Changeable via JavaScript.'),
+      '#description' => $this->t('Make this element non-editable with the user entered (e.g. via developer tools) value <strong>ignored</strong>. Useful for displaying default value. Changeable via JavaScript.'),
       '#return_value' => TRUE,
       '#weight' => 50,
     ];
     $form['form']['readonly'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Readonly'),
-      '#description' => $this->t('Make this element non-editable with the value <strong>submitted</strong>. Useful for displaying default value. Changeable via JavaScript.'),
+      '#description' => $this->t('Make this element non-editable with the user entered (e.g. via developer tools) value <strong>submitted</strong>. Useful for displaying default value. Changeable via JavaScript.'),
       '#return_value' => TRUE,
       '#weight' => 50,
     ];
@@ -2283,7 +2429,7 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['form']['icheck'] = [
       '#type' => 'select',
       '#title' => 'Enhance using iCheck',
-      '#description' => $this->t('Replaces @type element with jQuery <a href=":href">iCheck</a> boxes.', ['@type' => Unicode::strtolower($this->getPluginLabel()), ':href' => 'http://icheck.fronteed.com/']),
+      '#description' => $this->t('Replaces @type element with jQuery <a href=":href">iCheck</a> boxes.', ['@type' => mb_strtolower($this->getPluginLabel()), ':href' => 'http://icheck.fronteed.com/']),
       '#empty_option' => $this->t('- Default -'),
       '#options' => [
         (string) $this->t('Minimal') => [
@@ -2446,6 +2592,17 @@ public function form(array $form, FormStateInterface $form_state) {
       '#type' => 'webform_element_states',
       '#state_options' => $this->getElementStateOptions(),
       '#selector_options' => $webform->getElementsSelectorOptions(),
+      '#selector_sources' => $webform->getElementsSelectorSourceValues(),
+      '#disabled_message' => TRUE,
+    ];
+    $form['conditional_logic']['states_clear'] = [
+      '#type' => 'checkbox',
+      '#title' => 'Clear value(s) when hidden',
+      '#return_value' => TRUE,
+      '#description' => ($this instanceof ContainerBase) ?
+        $this->t("When this container is hidden all this container's subelement values will be cleared.")
+        :
+        $this->t("When this element is hidden, this element's value will be cleared."),
     ];
     if ($this->hasProperty('states') && $this->hasProperty('required')) {
       $form['conditional_logic']['states_required_message'] = [
@@ -2476,17 +2633,16 @@ public function form(array $form, FormStateInterface $form_state) {
         '#type' => 'webform_codemirror',
         '#mode' => 'yaml',
         '#title' => $this->t('Default value'),
-        '#description' => $this->t('The default value of the webform element.'),
       ];
     }
     else {
       $form['default']['default_value'] = [
         '#type' => 'textfield',
         '#title' => $this->t('Default value'),
-        '#description' => $this->t('The default value of the webform element.'),
         '#maxlength' => NULL,
       ];
     }
+    $form['default']['default_value']['#description'] = $this->t('The default value of the webform element.');
     if ($this->hasProperty('multiple')) {
       $form['default']['default_value']['#description'] .= ' ' . $this->t('For multiple options, use commas to separate multiple defaults.');
     }
@@ -2501,6 +2657,7 @@ public function form(array $form, FormStateInterface $form_state) {
           ':input[name="properties[multiple][container][cardinality_number]"]' => ['value' => 1],
         ],
       ],
+      '#attributes' => ['data-webform-states-no-clear' => TRUE],
     ];
     $form['multiple']['multiple__header'] = [
       '#type' => 'checkbox',
@@ -2513,69 +2670,119 @@ public function form(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Table header label'),
       '#description' => $this->t('This is used as the table header for this webform element when displaying multiple values.'),
     ];
-    $form['multiple']['multiple__label'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Item label'),
-      '#attributes' => ['data-webform-states-no-clear' => TRUE],
-      '#description' => $this->t('This is used as the item label for this webform element when displaying multiple values.'),
-    ];
-    $form['multiple']['multiple__labels'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Item labels'),
-      '#attributes' => ['data-webform-states-no-clear' => TRUE],
-      '#description' => $this->t('This is used as the items label for this webform element when displaying multiple values.'),
+    $form['multiple']['multiple__no_items_message'] = [
+      '#type' => 'webform_html_editor',
+      '#title' => $this->t('No items message'),
+      '#description' => $this->t('This is used when there are no items entered.'),
     ];
     $form['multiple']['multiple__min_items'] = [
       '#type' => 'number',
       '#title' => $this->t('Minimum amount of items'),
-      '#attributes' => ['data-webform-states-no-clear' => TRUE],
-      '#required' => TRUE,
-      '#min' => 1,
+      '#description' => $this->t('Minimum items defaults to 0 for optional elements and 1 for required elements.'),
+      '#min' => 0,
       '#max' => 20,
     ];
     $form['multiple']['multiple__empty_items'] = [
       '#type' => 'number',
       '#title' => $this->t('Number of empty items'),
-      '#attributes' => ['data-webform-states-no-clear' => TRUE],
       '#required' => TRUE,
       '#min' => 0,
       '#max' => 20,
     ];
-    $form['multiple']['multiple__add_more'] = [
-      '#type' => 'number',
-      '#title' => $this->t('Number of add more items'),
-      '#attributes' => ['data-webform-states-no-clear' => TRUE],
-      '#required' => TRUE,
-      '#min' => 1,
-      '#max' => 20,
-    ];
     $form['multiple']['multiple__sorting'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Allow users to sort elements.'),
       '#description' => $this->t('If unchecked, the elements will no longer be sortable.'),
-      '#attributes' => ['data-webform-states-no-clear' => TRUE],
       '#return_value' => TRUE,
     ];
     $form['multiple']['multiple__operations'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Allow users to add/remove elements.'),
       '#description' => $this->t('If unchecked, the add/remove (+/x) buttons will be removed from each table row.'),
-      '#attributes' => ['data-webform-states-no-clear' => TRUE],
       '#return_value' => TRUE,
     ];
+    $form['multiple']['multiple__add_more'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Allows users to add more items'),
+      '#description' => $this->t('If checked, an add more input will be added below the multiple values.'),
+      '#return_value' => TRUE,
+    ];
+    $form['multiple']['multiple__add_more_container'] = [
+      '#type' => 'container',
+      '#states' => [
+        'visible' => [
+          ':input[name="properties[multiple__add_more]"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+    $form['multiple']['multiple__add_more_container']['multiple__add_more_button_label'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Add more button label'),
+      '#description' => $this->t('This is used as the add more items button label for this webform element when displaying multiple values.'),
+    ];
+    $form['multiple']['multiple__add_more_container']['multiple__add_more_input_label'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Add more input label'),
+      '#description' => $this->t('This is used as the add more items input label for this webform element when displaying multiple values.'),
+    ];
+    $form['multiple']['multiple__add_more_container']['multiple__add_more_button_label'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Add more button label'),
+      '#description' => $this->t('This is used as the add more items button label for this webform element when displaying multiple values.'),
+    ];
+    $form['multiple']['multiple__add_more_container']['multiple__add_more_items'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Number of add more items'),
+      '#required' => TRUE,
+      '#min' => 1,
+      '#max' => 20,
+    ];
 
     /* Wrapper attributes */
 
     $form['wrapper_attributes'] = [
       '#type' => 'details',
-      '#title' => $this->t('Wrapper attributes'),
+      '#title' => ($this->hasProperty('wrapper_type')) ?
+        $this->t('Wrapper type and attributes') :
+        $this->t('Wrapper attributes'),
     ];
+    $form['wrapper_attributes']['wrapper_type'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Wrapper type'),
+      '#options' => [
+        'fieldset' => $this->t('Fieldset'),
+        'form_element' => $this->t('Form element'),
+        'container' => $this->t('Container'),
+      ],
+      '#description' => '<b>' . t('Fieldset') . ':</b> ' . t('Wraps inputs in a fieldset.') . ' <strong>' . t('Recommended') . '</strong>' .
+        '<br/><br/><b>' . t('Form element') . ':</b> ' . t('Wraps inputs in a basic form element with title and description.') .
+        '<br/><br/><b>' . t('Container') . ':</b> ' . t('Wraps inputs in a basic div with no title or description.'),
+    ];
+    // Hide element description and display when using a container wrapper.
+    if ($this->hasProperty('wrapper_type')) {
+      $form['element_description']['#states'] = [
+        '!visible' => [
+          ':input[name="properties[wrapper_type]"]' => ['value' => 'container'],
+        ],
+      ];
+      $form['form']['display_container']['#states'] = [
+        '!visible' => [
+          ':input[name="properties[wrapper_type]"]' => ['value' => 'container'],
+        ],
+      ];
+      $form['form']['field_container']['#states'] = [
+        '!visible' => [
+          ':input[name="properties[wrapper_type]"]' => ['value' => 'container'],
+        ],
+      ];
+    }
+
     $form['wrapper_attributes']['wrapper_attributes'] = [
       '#type' => 'webform_element_attributes',
       '#title' => $this->t('Wrapper'),
-      '#class__description' => $this->t("Apply classes to the element's wrapper around both the field and its label. Select 'custom...' to enter custom classes."),
+      '#class__description' => $this->t("Apply classes to the element's wrapper around both the field and its label. Select 'custom…' to enter custom classes."),
       '#style__description' => $this->t("Apply custom styles to the element's wrapper around both the field and its label."),
-      '#attributes__description' => $this->t("Enter additional attributes to be added the element's wrapper."),
+      '#attributes__description' => $this->t("Enter additional attributes to be added to the element's wrapper."),
       '#classes' => $this->configFactory->get('webform.settings')->get('element.wrapper_classes'),
     ];
 
@@ -2591,6 +2798,34 @@ public function form(array $form, FormStateInterface $form_state) {
       '#classes' => $this->configFactory->get('webform.settings')->get('element.classes'),
     ];
 
+    /* Label attributes */
+
+    $form['label_attributes'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Label attributes'),
+    ];
+    $form['label_attributes']['label_attributes'] = [
+      '#type' => 'webform_element_attributes',
+      '#title' => $this->t('Label'),
+      '#class__description' => $this->t("Apply classes to the element's label."),
+      '#style__description' => $this->t("Apply custom styles to the element's label."),
+      '#attributes__description' => $this->t("Enter additional attributes to be added to the element's label."),
+    ];
+
+    /* Summary attributes */
+
+    $form['summary_attributes'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Summary attributes'),
+    ];
+    $form['summary_attributes']['summary_attributes'] = [
+      '#type' => 'webform_element_attributes',
+      '#title' => $this->t('Summary'),
+      '#class__description' => $this->t("Apply classes to the details' summary around both the field and its label."),
+      '#style__description' => $this->t("Apply custom styles to the details' summary."),
+      '#attributes__description' => $this->t("Enter additional attributes to be added to the details' summary."),
+    ];
+
     /* Submission display */
     $has_edit_twig_access = TwigExtension::hasEditTwigAccess();
 
@@ -2612,9 +2847,9 @@ public function form(array $form, FormStateInterface $form_state) {
     $format = isset($element_properties['format']) ? $element_properties['format'] : NULL;
     $format_custom = ($has_edit_twig_access || $format === 'custom');
     if ($format_custom) {
-      $form['display']['item']['format']['#options'] += ['custom' => $this->t('Custom...')];
+      $form['display']['item']['format']['#options'] += ['custom' => $this->t('Custom…')];
     }
-    $custom_states = [
+    $format_custom_states = [
       'visible' => [':input[name="properties[format]"]' => ['value' => 'custom']],
       'required' => [':input[name="properties[format]"]' => ['value' => 'custom']],
     ];
@@ -2622,47 +2857,47 @@ public function form(array $form, FormStateInterface $form_state) {
       '#type' => 'webform_codemirror',
       '#mode' => 'twig',
       '#title' => $this->t('Item format custom HTML'),
-      '#description' => $this->t('The HTML to display for a single element value. You may include HTML or <a href=":href">Twig</a>. You may enter data from the submission as per the "Replacement patterns" below.', [':href' => 'http://twig.sensiolabs.org/documentation']),
-      '#states' => $custom_states,
+      '#description' => $this->t('The HTML to display for a single element value. You may include HTML or <a href=":href">Twig</a>. You may enter data from the submission as per the "variables" below.', [':href' => 'http://twig.sensiolabs.org/documentation']),
+      '#states' => $format_custom_states,
       '#access' => $format_custom,
     ];
     $form['display']['item']['format_text'] = [
       '#type' => 'webform_codemirror',
       '#mode' => 'twig',
       '#title' => $this->t('Item format custom Text'),
-      '#description' => $this->t('The text to display for a single element value. You may include <a href=":href">Twig</a>. You may enter data from the submission as per the "Replacement patterns" below.', [':href' => 'http://twig.sensiolabs.org/documentation']),
-      '#states' => $custom_states,
+      '#description' => $this->t('The text to display for a single element value. You may include <a href=":href">Twig</a>. You may enter data from the submission as per the "variables" below.', [':href' => 'http://twig.sensiolabs.org/documentation']),
+      '#states' => $format_custom_states,
       '#access' => $format_custom,
     ];
-    $items = [
-      'value' => '{{ value }}',
-    ];
-    $formats = $this->getItemFormats();
-    foreach ($formats as $format_name => $format) {
-      if (is_array($format)) {
-        foreach ($format as $sub_format_name => $sub_format) {
-          $items["item['$sub_format_name']"] = "{{ item['$sub_format_name'] }}";
+    if ($has_edit_twig_access) {
+      // Containers use the 'children' variable and inputs use the
+      // 'value' variable.
+      $twig_variables = ($this instanceof ContainerBase) ? ['children' => '{{ children }}'] : ['value' => '{{ value }}'];
+
+      // Composite Twig variables.
+      if ($this instanceof WebformCompositeBase) {
+        // Add composite elements to items.
+        $composite_elements = $this->getCompositeElements();
+        foreach ($composite_elements as $composite_key => $composite_element) {
+          $twig_variables["element.$composite_key"] = "{{ element.$composite_key }}";
         }
       }
-      else {
-        $items["item.$format_name"] = "{{ item.$format_name }}";
+
+      $formats = $this->getItemFormats();
+      foreach ($formats as $format_name => $format) {
+        if (is_array($format)) {
+          foreach ($format as $sub_format_name => $sub_format) {
+            $twig_variables["item['$sub_format_name']"] = "{{ item['$sub_format_name'] }}";
+          }
+        }
+        else {
+          $twig_variables["item.$format_name"] = "{{ item.$format_name }}";
+        }
       }
+      $form['display']['item']['twig'] = TwigExtension::buildTwigHelp($twig_variables);
+      $form['display']['item']['twig']['#states'] = $format_custom_states;
+      WebformElementHelper::setPropertyRecursive($form['display']['item']['twig'], '#access', TRUE);
     }
-    $form['display']['item']['patterns'] = [
-      '#type' => 'details',
-      '#title' => $this->t('Replacement patterns'),
-      '#access' => $has_edit_twig_access,
-      '#states' => $custom_states,
-      '#value' => [
-        'description' => [
-          '#markup' => '<p>' . $this->t('The following replacement tokens are available for single element value.') . '</p>',
-        ],
-        'items' => [
-          '#theme' => 'item_list',
-          '#items' => $items,
-        ],
-      ],
-    ];
 
     // Items.
     $form['display']['items'] = [
@@ -2685,9 +2920,9 @@ public function form(array $form, FormStateInterface $form_state) {
     $format_items = isset($element_properties['format_items']) ? $element_properties['format_items'] : NULL;
     $format_items_custom = ($has_edit_twig_access || $format_items === 'custom');
     if ($format_items_custom) {
-      $form['display']['items']['format_items']['#options'] += ['custom' => $this->t('Custom...')];
+      $form['display']['items']['format_items']['#options'] += ['custom' => $this->t('Custom…')];
     }
-    $custom_states = [
+    $format_items_custom_states = [
       'visible' => [':input[name="properties[format_items]"]' => ['value' => 'custom']],
       'required' => [':input[name="properties[format_items]"]' => ['value' => 'custom']],
     ];
@@ -2695,36 +2930,39 @@ public function form(array $form, FormStateInterface $form_state) {
       '#type' => 'webform_codemirror',
       '#mode' => 'twig',
       '#title' => $this->t('Items format custom HTML'),
-      '#description' => $this->t('The HTML to display for multiple element values. You may include HTML or <a href=":href">Twig</a>. You may enter data from the submission as per the "Replacement patterns" below.', [':href' => 'http://twig.sensiolabs.org/documentation']),
-      '#states' => $custom_states,
+      '#description' => $this->t('The HTML to display for multiple element values. You may include HTML or <a href=":href">Twig</a>. You may enter data from the submission as per the "variables" below.', [':href' => 'http://twig.sensiolabs.org/documentation']),
+      '#states' => $format_items_custom_states,
       '#access' => $format_items_custom,
     ];
     $form['display']['items']['format_items_text'] = [
       '#type' => 'webform_codemirror',
       '#mode' => 'twig',
       '#title' => $this->t('Items format custom Text'),
-      '#description' => $this->t('The text to display for multiple element values. You may include <a href=":href">Twig</a>. You may enter data from the submission as per the "Replacement patterns" below.', [':href' => 'http://twig.sensiolabs.org/documentation']),
-      '#states' => $custom_states,
+      '#description' => $this->t('The text to display for multiple element values. You may include <a href=":href">Twig</a>. You may enter data from the submission as per the "variables" below.', [':href' => 'http://twig.sensiolabs.org/documentation']),
+      '#states' => $format_items_custom_states,
       '#access' => $format_items_custom,
     ];
-    $items = [
-      '{{ value }}',
-      '{{ items }}',
-    ];
-    $form['display']['items']['patterns'] = [
+    if ($format_items_custom) {
+      $twig_variables = [
+        '{{ value }}',
+        '{{ items }}',
+      ];
+      $form['display']['items']['twig'] = TwigExtension::buildTwigHelp($twig_variables);
+      $form['display']['items']['twig']['#states'] = $format_items_custom_states;
+      WebformElementHelper::setPropertyRecursive($form['display']['items']['twig'], '#access', TRUE);
+    }
+
+    $form['display']['format_attributes'] = [
       '#type' => 'details',
-      '#title' => $this->t('Replacement patterns'),
-      '#access' => $has_edit_twig_access,
-      '#states' => $custom_states,
-      '#value' => [
-        'description' => [
-          '#markup' => '<p>' . $this->t('The following replacement tokens are available for multiple element values.') . '</p>',
-        ],
-        'items' => [
-          '#theme' => 'item_list',
-          '#items' => $items,
-        ],
-      ],
+      '#title' => $this->t('Display wrapper attributes'),
+    ];
+    $form['display']['format_attributes']['format_attributes'] = [
+      '#type' => 'webform_element_attributes',
+      '#title' => $this->t('Display'),
+      '#class__description' => $this->t("Apply classes to the element's display wrapper. Select 'custom…' to enter custom classes."),
+      '#style__description' => $this->t("Apply custom styles to the element's display wrapper."),
+      '#attributes__description' => $this->t("Enter additional attributes to be added to the element's display wrapper."),
+      '#classes' => $this->configFactory->get('webform.settings')->get('element.wrapper_classes'),
     ];
 
     /* Administration */
@@ -2756,17 +2994,17 @@ public function form(array $form, FormStateInterface $form_state) {
 
     $operations = [
       'create' => [
-        '#title' => $this->t('Create webform submission'),
+        '#title' => $this->t('Create submission'),
         '#description' => $this->t('Select roles and users that should be able to populate this element when creating a new submission.'),
         '#open' => TRUE,
       ],
       'update' => [
-        '#title' => $this->t('Update webform submission'),
+        '#title' => $this->t('Update submission'),
         '#description' => $this->t('Select roles and users that should be able to update this element when updating an existing submission.'),
         '#open' => FALSE,
       ],
       'view' => [
-        '#title' => $this->t('View webform submission'),
+        '#title' => $this->t('View submission'),
         '#description' => $this->t('Select roles and users that should be able to view this element when viewing a submission.'),
         '#open' => FALSE,
       ],
@@ -2906,8 +3144,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#parents' => ['properties', 'custom'],
     ];
 
-    $form['token_tree_link'] = $this->tokenManager->buildTreeLink();
-
     $this->tokenManager->elementValidate($form);
 
     // Set custom properties.
@@ -2960,6 +3196,8 @@ protected function buildConfigurationFormTabs(array $form, FormStateInterface $f
           'multiple',
           'wrapper_attributes',
           'element_attributes',
+          'label_attributes',
+          'summary_attributes',
           'display',
           'admin',
           'custom',
@@ -3071,7 +3309,7 @@ protected function setConfigurationFormDefaultValue(array &$form, array &$elemen
       case 'radios':
       case 'select':
         // Handle invalid default_value throwing
-        // "An illegal choice has been detected..." error.
+        // "An illegal choice has been detected…" error.
         if (!is_array($default_value) && isset($property_element['#options'])) {
           $flattened_options = OptGroup::flattenOptions($property_element['#options']);
           if (!isset($flattened_options[$default_value])) {
diff --git a/web/modules/webform/src/Plugin/WebformElementInterface.php b/web/modules/webform/src/Plugin/WebformElementInterface.php
index 228257073048ee7417fd2a86dcf800c959222580..22fbd6dafda6dd13dcf19c5acedd7aa5e83937f2 100644
--- a/web/modules/webform/src/Plugin/WebformElementInterface.php
+++ b/web/modules/webform/src/Plugin/WebformElementInterface.php
@@ -3,6 +3,7 @@
 namespace Drupal\webform\Plugin;
 
 use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\PluginFormInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
@@ -86,7 +87,7 @@ public function hasProperty($property_name);
    * Get the URL for the element's API documentation.
    *
    * @return \Drupal\Core\Url|null
-   *   The the URL for the element's API documentation.
+   *   The URL for the element's API documentation.
    */
   public function getPluginApiUrl();
 
@@ -114,6 +115,14 @@ public function getPluginLabel();
    */
   public function getPluginDescription();
 
+  /**
+   * Gets the category of the plugin instance.
+   *
+   * @return string
+   *   The category of the plugin instance.
+   */
+  public function getPluginCategory();
+
   /**
    * Gets the type name (aka id) of the plugin instance with the 'webform_' prefix.
    *
@@ -249,6 +258,17 @@ public function hasMultipleWrapper();
    */
   public function hasMultipleValues(array $element);
 
+  /**
+   * Determine if the element is or includes a managed_file upload element.
+   *
+   * @param array $element
+   *   An element.
+   *
+   * @return bool
+   *   TRUE if the element is or includes a managed_file upload element.
+   */
+  public function hasManagedFiles(array $element);
+
   /**
    * Retrieves the default properties for the defined element type.
    *
@@ -326,7 +346,7 @@ public function finalize(array &$element, WebformSubmissionInterface $webform_su
    * @return bool
    *   TRUE is the element can be accessed by the user.
    *
-   * @see \Drupal\webform\Entity\Webform::checkAccessRules
+   * @see \Drupal\webform\WebformAccessRulesManagerInterface::checkWebformAccess
    */
   public function checkAccessRules($operation, array $element, AccountInterface $account = NULL);
 
@@ -335,10 +355,10 @@ public function checkAccessRules($operation, array $element, AccountInterface $a
    *
    * @param array $element
    *   An element.
-   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
-   *   A webform submission.
+   * @param \Drupal\Core\Entity\EntityInterface|null $entity
+   *   A webform or webform submission entity.
    */
-  public function replaceTokens(array &$element, WebformSubmissionInterface $webform_submission = NULL);
+  public function replaceTokens(array &$element, EntityInterface $entity = NULL);
 
   /**
    * Display element disabled warning.
@@ -558,6 +578,19 @@ public function getItemsDefaultFormat();
    */
   public function getItemsFormat(array $element);
 
+  /**
+   * Checks if an empty element is excluded.
+   *
+   * @param array $element
+   *   An element.
+   * @param array $options
+   *   An array of options.
+   *
+   * @return bool
+   *   TRUE if an empty element is excluded.
+   */
+  public function isEmptyExcluded(array $element, array $options);
+
   /****************************************************************************/
   // Preview method.
   /****************************************************************************/
@@ -711,9 +744,24 @@ public function getElementStateOptions();
    *
    * @return array
    *   An array of element selectors.
+   *
+   * @see \Drupal\webform\Entity\Webform::getElementsSelectorSourceOption
    */
   public function getElementSelectorOptions(array $element);
 
+  /**
+   * Get an element's selectors source values.
+   *
+   * @param array $element
+   *   An element.
+   *
+   * @return array
+   *   An array of element selectors source values.
+   *
+   * @see \Drupal\webform\Entity\Webform::getElementsSelectorSourceValues
+   */
+  public function getElementSelectorSourceValues(array $element);
+
   /**
    * Get an element's (sub)input selector value.
    *
diff --git a/web/modules/webform/src/Plugin/WebformElementManager.php b/web/modules/webform/src/Plugin/WebformElementManager.php
index 51e2bda8ad36f69bc9a8f4330d4487d70c12eab3..09047dc0755b9c023b60e25f52b5fe1c3edd5bd8 100644
--- a/web/modules/webform/src/Plugin/WebformElementManager.php
+++ b/web/modules/webform/src/Plugin/WebformElementManager.php
@@ -180,6 +180,18 @@ public function buildElement(array &$element, array $form, FormStateInterface $f
     $this->moduleHandler->alter($hooks, $element, $form_state, $context);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function processElement(array &$element) {
+    $element_plugin = $this->getElementInstance($element);
+    $element_plugin->initialize($element);
+    $element_plugin->prepare($element);
+    $element_plugin->finalize($element);
+    $element_plugin->setDefaultValue($element);
+    return $element;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -305,4 +317,11 @@ public function getAllProperties() {
     return $properties;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isExcluded($type) {
+    return $this->configFactory->get('webform.settings')->get('element.excluded_elements.' . $type) ? TRUE : FALSE;
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformElementManagerInterface.php b/web/modules/webform/src/Plugin/WebformElementManagerInterface.php
index 21ed7bdc877dff812ab63f57861cbc7361d2b4f8..1f28da3843f6022bbcc61f956d514159694be219 100644
--- a/web/modules/webform/src/Plugin/WebformElementManagerInterface.php
+++ b/web/modules/webform/src/Plugin/WebformElementManagerInterface.php
@@ -35,7 +35,7 @@ public function initializeElement(array &$element);
    * @param array $element
    *   An associative array containing an element with a #type property.
    * @param array $form
-   *    An associative array containing the structure of the form.
+   *   An associative array containing the structure of the form.
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The current state of the form.
    *
@@ -45,6 +45,21 @@ public function initializeElement(array &$element);
    */
   public function buildElement(array &$element, array $form, FormStateInterface $form_state);
 
+  /**
+   * Process a form element and apply webform element specific enhancements.
+   *
+   * This method allows any form API element to be enhanced using webform
+   * specific features include custom validation, external libraries,
+   * accessibility improvements, etc…
+   *
+   * @param array $element
+   *   An associative array containing an element with a #type property.
+   *
+   * @return array
+   *   The processed form element with webform element specific enhancements.
+   */
+  public function processElement(array &$element);
+
   /**
    * Invoke a method for a Webform element.
    *
@@ -60,7 +75,7 @@ public function buildElement(array &$element, array $form, FormStateInterface $f
    *   associative array as described above.
    *
    * @return mixed|null
-   *   Return result of the invoked method.  NULL will be returned if the
+   *   Return result of the invoked method. NULL will be returned if the
    *   element and/or method name does not exist.
    *
    * @see \Drupal\webform\WebformSubmissionForm::prepareElements
@@ -132,4 +147,15 @@ public function getTranslatableProperties();
    */
   public function getAllProperties();
 
+  /**
+   * Determine if an element type is excluded.
+   *
+   * @param string $type
+   *   The element type.
+   *
+   * @return bool
+   *   TRUE if the element is excluded.
+   */
+  public function isExcluded($type);
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformExporter/TabularBaseWebformExporter.php b/web/modules/webform/src/Plugin/WebformExporter/TabularBaseWebformExporter.php
index 776a5725752e681d5f5dbc6cff389547faeed44b..5ff951e277355644b7a1d95a4b846528bb6e6f12 100644
--- a/web/modules/webform/src/Plugin/WebformExporter/TabularBaseWebformExporter.php
+++ b/web/modules/webform/src/Plugin/WebformExporter/TabularBaseWebformExporter.php
@@ -112,7 +112,15 @@ protected function formatRecordFieldDefinitionValue(array &$record, WebformSubmi
     switch ($field_type) {
       case 'created':
       case 'changed':
-        $record[] = date('Y-m-d H:i:s', $webform_submission->get($field_name)->value);
+      case 'timestamp':
+        if (!empty($webform_submission->$field_name->value)) {
+          /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
+          $date_formatter = \Drupal::service('date.formatter');
+          $record[] = $date_formatter->format($webform_submission->$field_name->value, 'custom', 'Y-m-d H:i:s');
+        }
+        else {
+          $record[] = '';
+        }
         break;
 
       case 'entity_reference':
@@ -201,9 +209,12 @@ protected function getElements() {
 
     $export_options = $this->getConfiguration();
     $this->elements = $this->getWebform()->getElementsInitializedFlattenedAndHasValue('view');
+    // Replace tokens which can be used in an element's #title.
+    $this->elements = $this->tokenManager->replace($this->elements, $this->getWebform());
     if ($export_options['excluded_columns']) {
       $this->elements = array_diff_key($this->elements, $export_options['excluded_columns']);
     }
+
     return $this->elements;
   }
 
diff --git a/web/modules/webform/src/Plugin/WebformExporterBase.php b/web/modules/webform/src/Plugin/WebformExporterBase.php
index eee47e893db6cd3f56ac27eafda7aa2e6ccc94a5..ee1d2ff0166ac1e8c13ae4a8c1a2844015b656fb 100644
--- a/web/modules/webform/src/Plugin/WebformExporterBase.php
+++ b/web/modules/webform/src/Plugin/WebformExporterBase.php
@@ -146,6 +146,13 @@ public function isArchive() {
     return $this->pluginDefinition['archive'];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function hasFiles() {
+    return $this->pluginDefinition['files'];
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -256,7 +263,7 @@ public function writeFooter() {}
    * {@inheritdoc}
    */
   public function getFileTempDirectory() {
-    return file_directory_temp();
+    return $this->configFactory->get('webform.settings')->get('export.temp_directory') ?: file_directory_temp();
   }
 
   /**
diff --git a/web/modules/webform/src/Plugin/WebformExporterInterface.php b/web/modules/webform/src/Plugin/WebformExporterInterface.php
index 2d0c6eb7850d2b7b0a8a6122be1e5e2fb2f0f472..ad2db2a36dc6ffed300b02c0f45890ecb042bc66 100644
--- a/web/modules/webform/src/Plugin/WebformExporterInterface.php
+++ b/web/modules/webform/src/Plugin/WebformExporterInterface.php
@@ -51,6 +51,14 @@ public function isExcluded();
    */
   public function isArchive();
 
+  /**
+   * Determine if exporter can include uploaded files (in a zipped archive).
+   *
+   * @return bool
+   *   TRUE if exporter can include uploaded files (in a zipped archive).
+   */
+  public function hasFiles();
+
   /**
    * Determine if exporter has options.
    *
diff --git a/web/modules/webform/src/Plugin/WebformHandler/ActionWebformHandler.php b/web/modules/webform/src/Plugin/WebformHandler/ActionWebformHandler.php
index 7ed0d3f46f3cc6968b99cc4bd1f62d6da3cad45b..ca8cea2f2c15aa25f83504d861c5c01e2e187835 100644
--- a/web/modules/webform/src/Plugin/WebformHandler/ActionWebformHandler.php
+++ b/web/modules/webform/src/Plugin/WebformHandler/ActionWebformHandler.php
@@ -25,6 +25,7 @@
  *   cardinality = \Drupal\webform\Plugin\WebformHandlerInterface::CARDINALITY_UNLIMITED,
  *   results = \Drupal\webform\Plugin\WebformHandlerInterface::RESULTS_PROCESSED,
  *   submission = \Drupal\webform\Plugin\WebformHandlerInterface::SUBMISSION_OPTIONAL,
+ *   tokens = TRUE,
  * )
  */
 class ActionWebformHandler extends WebformHandlerBase {
@@ -65,7 +66,7 @@ public static function create(ContainerInterface $container, array $configuratio
    */
   public function getSummary() {
     $configuration = $this->getConfiguration();
-    $this->configuration = $configuration['settings'];
+    $settings = $configuration['settings'];
 
     // Get state labels.
     $states = [
@@ -75,7 +76,7 @@ public function getSummary() {
       WebformSubmissionInterface::STATE_UPDATED => $this->t('Updated'),
       WebformSubmissionInterface::STATE_LOCKED => $this->t('Locked'),
     ];
-    $this->configuration['states'] = array_intersect_key($states, array_combine($this->configuration['states'], $this->configuration['states']));
+    $settings['states'] = array_intersect_key($states, array_combine($settings['states'], $settings['states']));
 
     // Get message type.
     $message_types = [
@@ -84,15 +85,15 @@ public function getSummary() {
       'warning' => t('Warning'),
       'info' => t('Info'),
     ];
-    $this->configuration['message'] = $this->configuration['message'] ? WebformHtmlEditor::checkMarkup($this->configuration['message']) : NULL;
-    $this->configuration['message_type'] = $message_types[$this->configuration['message_type']];
+    $settings['message'] = $settings['message'] ? WebformHtmlEditor::checkMarkup($settings['message']) : NULL;
+    $settings['message_type'] = $message_types[$settings['message_type']];
 
     // Get data element keys.
-    $data = Yaml::decode($this->configuration['data']) ?: [];
-    $this->configuration['data'] = array_keys($data);
+    $data = Yaml::decode($settings['data']) ?: [];
+    $settings['data'] = array_keys($data);
 
     return [
-      '#settings' => $this->configuration,
+      '#settings' => $settings,
     ] + parent::getSummary();
   }
 
@@ -126,10 +127,10 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#type' => 'checkboxes',
       '#title' => $this->t('Execute'),
       '#options' => [
-        WebformSubmissionInterface::STATE_DRAFT => $this->t('...when <b>draft</b> is saved.'),
-        WebformSubmissionInterface::STATE_CONVERTED => $this->t('...when anonymous submission is <b>converted</b> to authenticated.'),
-        WebformSubmissionInterface::STATE_COMPLETED => $this->t('...when submission is <b>completed</b>.'),
-        WebformSubmissionInterface::STATE_UPDATED => $this->t('...when submission is <b>updated</b>.'),
+        WebformSubmissionInterface::STATE_DRAFT => $this->t('…when <b>draft</b> is saved.'),
+        WebformSubmissionInterface::STATE_CONVERTED => $this->t('…when anonymous submission is <b>converted</b> to authenticated.'),
+        WebformSubmissionInterface::STATE_COMPLETED => $this->t('…when submission is <b>completed</b>.'),
+        WebformSubmissionInterface::STATE_UPDATED => $this->t('…when submission is <b>updated</b>.'),
       ],
       '#required' => TRUE,
       '#access' => $results_disabled ? FALSE : TRUE,
@@ -208,7 +209,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
         '#rows' => $elements_rows,
       ],
     ];
-    $form['actions']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['actions']['token_tree_link'] = $this->buildTokenTreeElement();
 
     // Development.
     $form['development'] = [
@@ -223,9 +224,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#default_value' => $this->configuration['debug'],
     ];
 
-    $this->tokenManager->elementValidate($form);
+    $this->elementTokenValidate($form);
 
-    return $this->setSettingsParentsRecursively($form);
+    return $this->setSettingsParents($form);
   }
 
   /**
@@ -251,7 +252,7 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
    */
   public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
     parent::submitConfigurationForm($form, $form_state);
-    parent::applyFormStateToConfiguration($form_state);
+    $this->applyFormStateToConfiguration($form_state);
 
     // Cleanup states.
     $this->configuration['states'] = array_values(array_filter($this->configuration['states']));
@@ -300,14 +301,14 @@ protected function executeAction(WebformSubmissionInterface $webform_submission)
     // Append notes.
     if ($this->configuration['notes']) {
       $notes = rtrim($webform_submission->getNotes());
-      $notes .= ($notes ? PHP_EOL . PHP_EOL : '') . $this->tokenManager->replace($this->configuration['notes'], $webform_submission);
+      $notes .= ($notes ? PHP_EOL . PHP_EOL : '') . $this->replaceTokens($this->configuration['notes'], $webform_submission);
       $webform_submission->setNotes($notes);
     }
 
     // Set data.
     if ($this->configuration['data']) {
       $data = Yaml::decode($this->configuration['data']);
-      $data = $this->tokenManager->replace($data, $webform_submission);
+      $data = $this->replaceTokens($data, $webform_submission);
       foreach ($data as $key => $value) {
         $webform_submission->setElementData($key, $value);
       }
@@ -316,10 +317,10 @@ protected function executeAction(WebformSubmissionInterface $webform_submission)
     // Display message.
     if ($this->configuration['message']) {
       $message = WebformHtmlEditor::checkMarkup(
-        $this->tokenManager->replace($this->configuration['message'], $webform_submission)
+        $this->replaceTokens($this->configuration['message'], $webform_submission)
       );
       $message_type = $this->configuration['message_type'];
-      drupal_set_message(\Drupal::service('renderer')->renderPlain($message), $message_type);
+      $this->messenger()->addMessage(\Drupal::service('renderer')->renderPlain($message), $message_type);
     }
 
     // Resave the webform submission without trigger any hooks or handlers.
@@ -396,7 +397,7 @@ protected function displayDebug(WebformSubmissionInterface $webform_submission)
       '#wrapper_attributes' => ['class' => ['container-inline'], 'style' => 'margin: 0'],
     ];
 
-    drupal_set_message(\Drupal::service('renderer')->renderPlain($build), 'warning', TRUE);
+    $this->messenger()->addWarning(\Drupal::service('renderer')->renderPlain($build), TRUE);
   }
 
 }
diff --git a/web/modules/webform/src/Plugin/WebformHandler/DebugWebformHandler.php b/web/modules/webform/src/Plugin/WebformHandler/DebugWebformHandler.php
index 12b2fb1a03f7fb1dda166701ed15f074af9b6fab..748a0b83b35c196791fa0571249ea10225d60780 100644
--- a/web/modules/webform/src/Plugin/WebformHandler/DebugWebformHandler.php
+++ b/web/modules/webform/src/Plugin/WebformHandler/DebugWebformHandler.php
@@ -2,10 +2,10 @@
 
 namespace Drupal\webform\Plugin\WebformHandler;
 
-use Drupal\Core\Serialization\Yaml;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\webform\Utility\WebformYaml;
 use Drupal\webform\Plugin\WebformHandlerBase;
+use Drupal\webform\Utility\WebformElementHelper;
+use Drupal\webform\Utility\WebformYaml;
 use Drupal\webform\WebformSubmissionInterface;
 
 /**
@@ -27,8 +27,10 @@ class DebugWebformHandler extends WebformHandlerBase {
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) {
-    $build = ['#markup' => 'Submitted values are:<pre>' . WebformYaml::tidy(Yaml::encode($webform_submission->getData())) . '</pre>'];
-    drupal_set_message(\Drupal::service('renderer')->renderPlain($build), 'warning');
+    $data = $webform_submission->getData();
+    WebformElementHelper::convertRenderMarkupToStrings($data);
+    $build = ['#markup' => 'Submitted values are:<pre>' . WebformYaml::encode($data) . '</pre>'];
+    $this->messenger()->addWarning(\Drupal::service('renderer')->renderPlain($build));
   }
 
 }
diff --git a/web/modules/webform/src/Plugin/WebformHandler/EmailWebformHandler.php b/web/modules/webform/src/Plugin/WebformHandler/EmailWebformHandler.php
index f858c7ee67ba3ebcfbbfdee0614b868b6279be4a..fdf6787d4b418013ce60c21a4c42f6652f136584 100644
--- a/web/modules/webform/src/Plugin/WebformHandler/EmailWebformHandler.php
+++ b/web/modules/webform/src/Plugin/WebformHandler/EmailWebformHandler.php
@@ -2,30 +2,29 @@
 
 namespace Drupal\webform\Plugin\WebformHandler;
 
+use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\NestedArray;
-use Drupal\Component\Utility\Xss;
-use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Logger\LoggerChannelFactoryInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
 use Drupal\Core\Mail\MailManagerInterface;
-use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Render\Markup;
+use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\file\Entity\File;
 use Drupal\webform\Element\WebformMessage;
 use Drupal\webform\Element\WebformSelectOther;
-use Drupal\webform\Plugin\WebformElement\WebformManagedFileBase;
-use Drupal\webform\Twig\TwigExtension;
-use Drupal\webform\Utility\WebformElementHelper;
-use Drupal\webform\Utility\WebformOptionsHelper;
+use Drupal\webform\Plugin\WebformElement\WebformCompositeBase;
 use Drupal\webform\Plugin\WebformElementManagerInterface;
 use Drupal\webform\Plugin\WebformHandlerBase;
 use Drupal\webform\Plugin\WebformHandlerMessageInterface;
+use Drupal\webform\Twig\TwigExtension;
+use Drupal\webform\Utility\Mail;
+use Drupal\webform\Utility\WebformElementHelper;
+use Drupal\webform\Utility\WebformOptionsHelper;
 use Drupal\webform\WebformSubmissionConditionsValidatorInterface;
 use Drupal\webform\WebformSubmissionInterface;
 use Drupal\webform\WebformThemeManagerInterface;
@@ -43,6 +42,7 @@
  *   cardinality = \Drupal\webform\Plugin\WebformHandlerInterface::CARDINALITY_UNLIMITED,
  *   results = \Drupal\webform\Plugin\WebformHandlerInterface::RESULTS_PROCESSED,
  *   submission = \Drupal\webform\Plugin\WebformHandlerInterface::SUBMISSION_OPTIONAL,
+ *   tokens = TRUE,
  * )
  */
 class EmailWebformHandler extends WebformHandlerBase implements WebformHandlerMessageInterface {
@@ -59,9 +59,16 @@ class EmailWebformHandler extends WebformHandlerBase implements WebformHandlerMe
 
   /**
    * Default option value.
+   *
+   * @see \Drupal\webform\Plugin\WebformHandler\EmailWebformHandler::getMessageEmails
    */
   const DEFAULT_OPTION = '_default_';
 
+  /**
+   * Default value. (This is used by the handler's settings.)
+   */
+  const DEFAULT_VALUE = '_default';
+
   /**
    * The current user.
    *
@@ -166,17 +173,20 @@ public static function create(ContainerInterface $container, array $configuratio
    */
   public function getSummary() {
     $settings = $this->getEmailConfiguration();
+
     // Simplify the [webform_submission:values:.*] tokens.
     array_walk($settings, function (&$value, $key) {
       if (is_string($value)) {
         $value = preg_replace('/\[webform:([^:]+)\]/', '[\1]', $value);
         $value = preg_replace('/\[webform_role:([^:]+)\]/', '[\1]', $value);
+        $value = preg_replace('/\[webform_access:type:([^:]+)\]/', '[\1]', $value);
         $value = preg_replace('/\[webform_submission:(?:node|source_entity|values):([^]]+)\]/', '[\1]', $value);
         $value = preg_replace('/\[webform_submission:([^]]+)\]/', '[\1]', $value);
         $value = preg_replace('/(:raw|:value)(:html)?\]/', ']', $value);
       }
     });
 
+    // Set state.
     $states = [
       WebformSubmissionInterface::STATE_DRAFT => $this->t('Draft Saved'),
       WebformSubmissionInterface::STATE_CONVERTED => $this->t('Converted'),
@@ -186,31 +196,59 @@ public function getSummary() {
     ];
     $settings['states'] = array_intersect_key($states, array_combine($settings['states'], $settings['states']));
 
+    // Set theme name.
+    if ($settings['theme_name']) {
+      $settings['theme_name'] = $this->themeManager->getThemeName($settings['theme_name']);
+    }
+
     return [
       '#settings' => $settings,
     ] + parent::getSummary();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfiguration(array $configuration) {
+    parent::setConfiguration($configuration);
+
+    // Make sure 'default' is converted to '_default'.
+    // @see https://www.drupal.org/project/webform/issues/2980470
+    // @see webform_update_8131()
+    // @todo Webform 8.x-6.x: Remove the below code.
+    $default_configuration = $this->defaultConfiguration();
+    foreach ($this->configuration as $key => $value) {
+      if ($value === 'default'
+        && isset($default_configuration[$key])
+        && $default_configuration[$key] === static::DEFAULT_VALUE) {
+        $this->configuration[$key] = static::DEFAULT_VALUE;
+      }
+    }
+
+    return $this;
+  }
+
   /**
    * {@inheritdoc}
    */
   public function defaultConfiguration() {
     return [
       'states' => [WebformSubmissionInterface::STATE_COMPLETED],
-      'to_mail' => 'default',
+      'to_mail' => static::DEFAULT_VALUE,
       'to_options' => [],
       'cc_mail' => '',
       'cc_options' => [],
       'bcc_mail' => '',
       'bcc_options' => [],
-      'from_mail' => 'default',
+      'from_mail' => static::DEFAULT_VALUE,
       'from_options' => [],
-      'from_name' => 'default',
-      'subject' => 'default',
-      'body' => 'default',
+      'from_name' => static::DEFAULT_VALUE,
+      'subject' => static::DEFAULT_VALUE,
+      'body' => static::DEFAULT_VALUE,
       'excluded_elements' => [],
       'ignore_access' => FALSE,
       'exclude_empty' => TRUE,
+      'exclude_empty_checkbox' => FALSE,
       'html' => TRUE,
       'attachments' => FALSE,
       'twig' => FALSE,
@@ -219,6 +257,7 @@ public function defaultConfiguration() {
       'return_path' => '',
       'sender_mail' => '',
       'sender_name' => '',
+      'theme_name' => '',
     ];
   }
 
@@ -256,6 +295,7 @@ protected function getDefaultConfigurationValues() {
       'return_path' => $webform_settings->get('mail.default_return_path') ?: '',
       'sender_mail' => $webform_settings->get('mail.default_sender_mail') ?: '',
       'sender_name' => $webform_settings->get('mail.default_sender_name') ?: '',
+      'theme_name' => '',
     ];
 
     return $this->defaultValues;
@@ -279,13 +319,14 @@ protected function getDefaultConfigurationValue($name) {
    * Get mail configuration values.
    *
    * @return array
-   *   An associative array containing email configuration values.
+   *   An associative array containing email configuration values,
+   *   along with the default configuration values.
    */
-  protected function getEmailConfiguration() {
+  public function getEmailConfiguration() {
     $configuration = $this->getConfiguration();
     $email = [];
     foreach ($configuration['settings'] as $key => $value) {
-      $email[$key] = ($value === 'default') ? $this->getDefaultConfigurationValue($key) : $value;
+      $email[$key] = ($value === static::DEFAULT_VALUE) ? $this->getDefaultConfigurationValue($key) : $value;
     }
     return $email;
   }
@@ -299,36 +340,80 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
     // Get options, mail, and text elements as options (text/value).
     $text_element_options_value = [];
     $text_element_options_raw = [];
-    $options_element_options = [];
-    $mail_element_options = [];
     $name_element_options = [];
+    $mail_element_options = [];
+    $options_element_options = [];
 
     $elements = $this->webform->getElementsInitializedAndFlattened();
-    foreach ($elements as $key => $element) {
+    foreach ($elements as $element_key => $element) {
       $element_plugin = $this->elementManager->getElementInstance($element);
       if (!$element_plugin->isInput($element) || !isset($element['#type'])) {
         continue;
       }
 
-      $title = (isset($element['#title'])) ? new FormattableMarkup('@title (@key)', ['@title' => $element['#title'], '@key' => $key]) : $key;
-
-      $text_element_options_value["[webform_submission:values:$key:value]"] = $title;
-      $text_element_options_raw["[webform_submission:values:$key:raw]"] = $title;
+      // Set title.
+      $element_title = (isset($element['#title'])) ? new FormattableMarkup('@title (@key)', ['@title' => $element['#title'], '@key' => $element_key]) : $element_key;
 
+      // Add options element token, which can include multiple values.
       if (isset($element['#options'])) {
-        $options_element_options["[webform_submission:values:$key:raw]"] = $title;
+        $options_element_options["[webform_submission:values:$element_key:raw]"] = $element_title;
       }
-      elseif (in_array($element['#type'], ['email', 'hidden', 'value', 'select', 'radios', 'textfield', 'webform_email_multiple', 'webform_email_confirm'])) {
-        $mail_element_options["[webform_submission:values:$key:raw]"] = $title;
+
+      // Multiple value elements can NOT be used as a tokens.
+      if ($element_plugin->hasMultipleValues($element)) {
+        continue;
       }
 
-      // Name elements options can only include the 'webform_name' composite
-      // and single value elements.
-      if ($element['#type'] == 'webform_name') {
-        $name_element_options["[webform_submission:values:$key:value]"] = $title;
+      if (!$element_plugin->isComposite()) {
+        // Add text element value and raw tokens.
+        $text_element_options_value["[webform_submission:values:$element_key:value]"] = $element_title;
+        $text_element_options_raw["[webform_submission:values:$element_key:raw]"] = $element_title;
+
+        // Add name element token.
+        $name_element_options["[webform_submission:values:$element_key:raw]"] = $element_title;
+
+        // Add mail element token.
+        if (in_array($element['#type'], ['email', 'hidden', 'value', 'textfield', 'webform_email_multiple', 'webform_email_confirm'])) {
+          $mail_element_options["[webform_submission:values:$element_key:raw]"] = $element_title;
+        }
       }
-      elseif (!$element_plugin->isComposite() && !$element_plugin->hasMultipleValues($elements)) {
-        $name_element_options["[webform_submission:values:$key:raw]"] = $title;
+
+      // Allow 'webform_name' composite to be used a value token.
+      if ($element['#type'] === 'webform_name') {
+        $name_element_options["[webform_submission:values:$element_key:value]"] = $element_title;
+      }
+
+      // Handle composite sub elements.
+      if ($element_plugin instanceof WebformCompositeBase) {
+        $composite_elements = $element_plugin->getCompositeElements();
+        foreach ($composite_elements as $composite_key => $composite_element) {
+          $composite_element_plugin = $this->elementManager->getElementInstance($element);
+          if (!$composite_element_plugin->isInput($element) || !isset($composite_element['#type'])) {
+            continue;
+          }
+
+          // Set composite title.
+          if (isset($element['#title'])) {
+            $f_args = [
+              '@title' => $element['#title'],
+              '@composite_title' => $composite_element['#title'],
+              '@key' => $element_key,
+              '@composite_key' => $composite_key,
+            ];
+            $composite_title = new FormattableMarkup('@title: @composite_title (@key: @composite_key)', $f_args);
+          }
+          else {
+            $composite_title = "$element_key:$composite_key";
+          }
+
+          // Add name element token. Only applies to basic (not composite) elements.
+          $name_element_options["[webform_submission:values:$element_key:$composite_key:raw]"] = $composite_title;
+
+          // Add mail element token.
+          if (in_array($composite_element['#type'], ['email', 'webform_email_multiple', 'webform_email_confirm'])) {
+            $mail_element_options["[webform_submission:values:$element_key:$composite_key:raw]"] = $composite_title;
+          }
+        }
       }
     }
 
@@ -344,6 +429,23 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       }
     }
 
+    // Get email and name other.
+    $other_element_email_options = [
+      '[site:mail]' => 'Site email address',
+      '[current-user:mail]' => 'Current user email address [Authenticated only]',
+      '[webform:author:mail]' => 'Webform author email address',
+      '[webform_submission:user:mail]' => 'Webform submission owner email address [Authenticated only]',
+    ];
+    $other_element_name_options = [
+      '[site:name]' => 'Site name',
+      '[current-user:display-name]' => 'Current user display name',
+      '[current-user:account-name]' => 'Current user account name',
+      '[webform:author:display-name]' => 'Webform author display name',
+      '[webform:author:account-name]' => 'Webform author account name',
+      '[webform_submission:author:display-name]' => 'Webform submission author display name',
+      '[webform_submission:author:account-name]' => 'Webform submission author account name',
+    ];
+
     // Disable client-side HTML5 validation which is having issues with hidden
     // element validation.
     // @see http://stackoverflow.com/questions/22148080/an-invalid-form-control-with-name-is-not-focusable
@@ -355,19 +457,18 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#title' => $this->t('Send to'),
       '#open' => TRUE,
     ];
-    $form['to']['to_mail'] = $this->buildElement('to_mail', $this->t('To email'), $this->t('To email address'), $mail_element_options, $options_element_options, $roles_element_options, TRUE);
-    $form['to']['cc_mail'] = $this->buildElement('cc_mail', $this->t('CC email'), $this->t('CC email address'), $mail_element_options, $options_element_options, $roles_element_options, FALSE);
-    $form['to']['bcc_mail'] = $this->buildElement('bcc_mail', $this->t('BCC email'), $this->t('BCC email address'), $mail_element_options, $options_element_options, $roles_element_options, FALSE);
+    $form['to']['to_mail'] = $this->buildElement('to_mail', $this->t('To email'), $this->t('To email address'), TRUE, $mail_element_options, $options_element_options, $roles_element_options, $other_element_email_options);
+    $form['to']['cc_mail'] = $this->buildElement('cc_mail', $this->t('CC email'), $this->t('CC email address'), FALSE, $mail_element_options, $options_element_options, $roles_element_options, $other_element_email_options);
+    $form['to']['bcc_mail'] = $this->buildElement('bcc_mail', $this->t('BCC email'), $this->t('BCC email address'), FALSE, $mail_element_options, $options_element_options, $roles_element_options, $other_element_email_options);
     $token_types = ['webform', 'webform_submission'];
     // Show webform role tokens if they have been specified.
     if (!empty($roles_element_options)) {
       $token_types[] = 'webform_role';
     }
-
-    $form['to']['token_tree_link'] = $this->tokenManager->buildTreeLink(
-      $token_types,
-      $this->t('Use [webform_submission:values:ELEMENT_KEY:raw] to get plain text values.')
-    );
+    if ($this->moduleHandler->moduleExists('webform_access')) {
+      $token_types[] = 'webform_access';
+    }
+    $form['to']['token_tree_link'] = $this->buildTokenTreeElement($token_types);
 
     if (empty($roles_element_options) && $this->currentUser->hasPermission('administer webform')) {
       $form['to']['roles_message'] = [
@@ -386,12 +487,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#title' => $this->t('Send from'),
       '#open' => TRUE,
     ];
-    $form['from']['from_mail'] = $this->buildElement('from_mail', $this->t('From email'), $this->t('From email address'), $mail_element_options, $options_element_options, NULL, TRUE);
-    $form['from']['from_name'] = $this->buildElement('from_name', $this->t('From name'), $this->t('From name'), $name_element_options);
-    $form['from']['token_tree_link'] = $this->tokenManager->buildTreeLink(
-        ['webform', 'webform_submission'],
-        $this->t('Use [webform_submission:values:ELEMENT_KEY:raw] to get plain text values.')
-    );
+    $form['from']['from_mail'] = $this->buildElement('from_mail', $this->t('From email'), $this->t('From email address'), TRUE, $mail_element_options, $options_element_options, NULL, $other_element_email_options);
+    $form['from']['from_name'] = $this->buildElement('from_name', $this->t('From name'), $this->t('From name'), FALSE, $name_element_options, NULL, NULL, $other_element_name_options);
+    $form['from']['token_tree_link'] = $this->buildTokenTreeElement();
 
     // Message.
     $form['message'] = [
@@ -399,7 +497,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#title' => $this->t('Message'),
       '#open' => TRUE,
     ];
-    $form['message'] += $this->buildElement('subject', $this->t('Subject'), $this->t('subject'), $text_element_options_raw);
+    $form['message'] += $this->buildElement('subject', $this->t('Subject'), $this->t('subject'), FALSE, $text_element_options_raw);
 
     $has_edit_twig_access = (TwigExtension::hasEditTwigAccess() || $this->configuration['twig']);
 
@@ -408,11 +506,11 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
     // HTML (CKEditor), Plain text (CodeMirror), and Twig (CodeMirror)
     // custom body elements.
     $body_options = [];
-    $body_options[WebformSelectOther::OTHER_OPTION] = $this->t('Custom body...');
+    $body_options[WebformSelectOther::OTHER_OPTION] = $this->t('Custom body…');
     if ($has_edit_twig_access) {
-      $body_options['twig'] = $this->t('Twig template...');
+      $body_options['twig'] = $this->t('Twig template…');
     }
-    $body_options['default'] = $this->t('Default');
+    $body_options[static::DEFAULT_VALUE] = $this->t('Default');
     $body_options[(string) $this->t('Elements')] = $text_element_options_value;
 
     // Get default format.
@@ -451,13 +549,13 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#title' => $this->t('Body'),
       '#options' => $body_options,
       '#required' => TRUE,
-      '#parents' => ['settings', 'body'],
       '#default_value' => $body_select_default_value,
     ];
     foreach ($body_default_values as $format => $default_value) {
       if ($format == 'html') {
         $form['message']['body_custom_' . $format] = [
           '#type' => 'webform_html_editor',
+          '#format' => $this->configFactory->get('webform.settings')->get('html_editor.mail_format'),
         ];
       }
       else {
@@ -467,9 +565,8 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
         ];
       }
       $form['message']['body_custom_' . $format] += [
-        '#title' => $this->t('Body custom value (@format)', ['@label' => $format]),
+        '#title' => $this->t('Body custom value (@format)', ['@format' => $format]),
         '#title_display' => 'hidden',
-        '#parents' => ['settings', 'body_custom_' . $format],
         '#default_value' => $body_custom_default_values[$format],
         '#states' => [
           'visible' => [
@@ -482,6 +579,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
           ],
         ],
       ];
+      // Must set #parents because body_custom_* is not a configuration value.
+      // @see \Drupal\webform\Plugin\WebformHandler\EmailWebformHandler::validateConfigurationForm
+      $form['message']['body_custom_' . $format]['#parents'] = ['settings', 'body_custom_' . $format];
 
       // Default body.
       $form['message']['body_default_' . $format] = [
@@ -493,7 +593,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
         '#attributes' => ['readonly' => 'readonly', 'disabled' => 'disabled'],
         '#states' => [
           'visible' => [
-            ':input[name="settings[body]"]' => ['value' => 'default'],
+            ':input[name="settings[body]"]' => ['value' => static::DEFAULT_VALUE],
             ':input[name="settings[html]"]' => ['checked' => ($format == 'html') ? TRUE : FALSE],
           ],
         ],
@@ -505,7 +605,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#mode' => 'twig',
       '#title' => $this->t('Body custom value (Twig)'),
       '#title_display' => 'hidden',
-      '#parents' => ['settings', 'body_custom_twig'],
       '#default_value' => $body_custom_default_values['twig'],
       '#access' => $has_edit_twig_access,
       '#states' => [
@@ -516,6 +615,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
           ':input[name="settings[body]"]' => ['value' => 'twig'],
         ],
       ],
+      // Must set #parents because body_custom_twig is not a configuration value.
+      // @see \Drupal\webform\Plugin\WebformHandler\EmailWebformHandler::validateConfigurationForm
+      '#parents' => ['settings', 'body_custom_twig'],
     ];
     $form['message']['body_custom_twig_help'] = TwigExtension::buildTwigHelp() + [
       '#access' => $has_edit_twig_access,
@@ -526,10 +628,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       ],
     ];
     // Tokens.
-    $form['message']['token_tree_link'] = $this->tokenManager->buildTreeLink(
-      ['webform', 'webform_submission'],
-      $this->t('Use [webform_submission:values:ELEMENT_KEY:raw] to get plain text values and use [webform_submission:values:ELEMENT_KEY:value] to get HTML values.')
-    );
+    $form['message']['token_tree_link'] = $this->buildTokenTreeElement();
 
     // Elements.
     $form['elements'] = [
@@ -542,7 +641,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#type' => 'webform_excluded_elements',
       '#webform_id' => $this->webform->id(),
       '#default_value' => $this->configuration['excluded_elements'],
-      '#parents' => ['settings', 'excluded_elements'],
     ];
     $form['elements']['ignore_access'] = [
       '#type' => 'checkbox',
@@ -550,18 +648,21 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#description' => $this->t('If checked, access controls for included element will be ignored.'),
       '#return_value' => TRUE,
       '#default_value' => $this->configuration['ignore_access'],
-      '#parents' => ['settings', 'ignore_access'],
     ];
     $form['elements']['exclude_empty'] = [
       '#type' => 'checkbox',
       '#title' => t('Exclude empty elements'),
       '#return_value' => TRUE,
       '#default_value' => $this->configuration['exclude_empty'],
-      '#parents' => ['settings', 'exclude_empty'],
     ];
-
+    $form['elements']['exclude_empty_checkbox'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Exclude unselected checkboxes'),
+      '#return_value' => TRUE,
+      '#default_value' => $this->configuration['exclude_empty_checkbox'],
+    ];
     $elements = $this->webform->getElementsInitializedFlattenedAndHasValue();
-    foreach ($elements as $key => $element) {
+    foreach ($elements as $element) {
       if (!empty($element['#access_view_roles']) || !empty($element['#private'])) {
         $form['elements']['ignore_access_message'] = [
           '#type' => 'webform_message',
@@ -575,6 +676,35 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       }
     }
 
+    // Attachments.
+    $form['attachments'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Attachments'),
+      '#access' => $this->getWebform()->hasAttachments(),
+    ];
+    if (!$this->supportsAttachments()) {
+      $t_args = [
+        ':href_smtp' => 'https://www.drupal.org/project/smtp',
+        ':href_mailsystem' => 'https://www.drupal.org/project/mailsystem',
+        ':href_swiftmailer' => 'https://www.drupal.org/project/swiftmailer',
+      ];
+      $form['attachments']['attachments_message'] = [
+        '#type' => 'webform_message',
+        '#message_message' => $this->t('To send email attachments, please install and configure the <a href=":href_smtp">SMTP Authentication Support</a> module or the <a href=":href_mailsystem">Mail System</a> and <a href=":href_swiftmailer">SwiftMailer</a> module.', $t_args),
+        '#message_type' => 'warning',
+        '#message_close' => TRUE,
+        '#message_storage' => WebformMessage::STORAGE_SESSION,
+      ];
+    }
+    $form['attachments']['attachments'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Include files as attachments'),
+      '#description' => $this->t('If checked, only elements selected in the above email values will be attached the email.'),
+      '#return_value' => TRUE,
+      '#disabled' => !$this->supportsAttachments(),
+      '#default_value' => $this->configuration['attachments'],
+    ];
+
     // Additional.
     $results_disabled = $this->getWebform()->getSetting('results_disabled');
     $form['additional'] = [
@@ -586,14 +716,14 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#type' => 'checkboxes',
       '#title' => $this->t('Send email'),
       '#options' => [
-        WebformSubmissionInterface::STATE_DRAFT => $this->t('...when <b>draft</b> is saved.'),
-        WebformSubmissionInterface::STATE_CONVERTED => $this->t('...when anonymous submission is <b>converted</b> to authenticated.'),
-        WebformSubmissionInterface::STATE_COMPLETED => $this->t('...when submission is <b>completed</b>.'),
-        WebformSubmissionInterface::STATE_UPDATED => $this->t('...when submission is <b>updated</b>.'),
-        WebformSubmissionInterface::STATE_DELETED => $this->t('...when submission is <b>deleted</b>.'),
+        WebformSubmissionInterface::STATE_DRAFT => $this->t('…when <b>draft</b> is saved.'),
+        WebformSubmissionInterface::STATE_CONVERTED => $this->t('…when anonymous submission is <b>converted</b> to authenticated.'),
+        WebformSubmissionInterface::STATE_COMPLETED => $this->t('…when submission is <b>completed</b>.'),
+        WebformSubmissionInterface::STATE_UPDATED => $this->t('…when submission is <b>updated</b>.'),
+        WebformSubmissionInterface::STATE_DELETED => $this->t('…when submission is <b>deleted</b>.'),
+        WebformSubmissionInterface::STATE_LOCKED => $this->t('…when submission is <b>locked</b>.'),
       ],
       '#access' => $results_disabled ? FALSE : TRUE,
-      '#parents' => ['settings', 'states'],
       '#default_value' => $results_disabled ? [WebformSubmissionInterface::STATE_COMPLETED] : $this->configuration['states'],
     ];
     $form['additional']['states_message'] = [
@@ -607,13 +737,13 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       ],
     ];
     // Settings: Reply-to.
-    $form['additional']['reply_to'] = $this->buildElement('reply_to', $this->t('Reply-to email'), $this->t('Reply-to email address'), $mail_element_options, NULL, NULL, FALSE);
+    $form['additional']['reply_to'] = $this->buildElement('reply_to', $this->t('Reply-to email'), $this->t('Reply-to email address'), FALSE, $mail_element_options, NULL, NULL, $other_element_email_options);
     // Settings: Return path.
-    $form['additional']['return_path'] = $this->buildElement('return_path', $this->t('Return path'), $this->t('Return path email address'), $mail_element_options, NULL, NULL, FALSE);
+    $form['additional']['return_path'] = $this->buildElement('return_path', $this->t('Return path'), $this->t('Return path email address'), FALSE, $mail_element_options, NULL, NULL, $other_element_email_options);
     // Settings: Sender mail.
-    $form['additional']['sender_mail'] = $this->buildElement('sender_mail', $this->t('Sender email'), $this->t('Sender email address'), $mail_element_options, $options_element_options);
+    $form['additional']['sender_mail'] = $this->buildElement('sender_mail', $this->t('Sender email'), $this->t('Sender email address'), FALSE, $mail_element_options, $options_element_options, NULL, $other_element_email_options);
     // Settings: Sender name.
-    $form['additional']['sender_name'] = $this->buildElement('sender_name', $this->t('Sender name'), $this->t('Sender name'), $name_element_options);
+    $form['additional']['sender_name'] = $this->buildElement('sender_name', $this->t('Sender name'), $this->t('Sender name'), FALSE, $name_element_options, NULL, NULL, $other_element_name_options);
 
     // Settings: HTML.
     $form['additional']['html'] = [
@@ -621,17 +751,16 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#title' => $this->t('Send email as HTML'),
       '#return_value' => TRUE,
       '#access' => $this->supportsHtml(),
-      '#parents' => ['settings', 'html'],
       '#default_value' => $this->configuration['html'],
     ];
-    // Settings: Attachments.
-    $form['additional']['attachments'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Include files as attachments'),
-      '#return_value' => TRUE,
-      '#access' => $this->supportsAttachments(),
-      '#parents' => ['settings', 'attachments'],
-      '#default_value' => $this->configuration['attachments'],
+
+    // Setting: Themes.
+    $form['additional']['theme_name'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Theme to render this email'),
+      '#description' => $this->t('Select the theme that will be used to render this email.'),
+      '#options' => $this->themeManager->getThemeNames(),
+      '#default_value' => $this->configuration['theme_name'],
     ];
 
     // Development.
@@ -644,7 +773,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#title' => $this->t('Enable debugging'),
       '#description' => $this->t('If checked, sent emails will be displayed onscreen to all users.'),
       '#return_value' => TRUE,
-      '#parents' => ['settings', 'debug'],
       '#default_value' => $this->configuration['debug'],
     ];
 
@@ -652,9 +780,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
     // WORKAROUND: Convert all Render/Markup to strings.
     WebformElementHelper::convertRenderMarkupToStrings($form);
 
-    $this->tokenManager->elementValidate($form, $token_types);
+    $this->elementTokenValidate($form, $token_types);
 
-    return $form;
+    return $this->setSettingsParents($form);
   }
 
   /**
@@ -736,14 +864,17 @@ public function postDelete(WebformSubmissionInterface $webform_submission) {
    * {@inheritdoc}
    */
   public function getMessage(WebformSubmissionInterface $webform_submission) {
-    // Switch to default theme.
-    $this->themeManager->setDefaultTheme();
+    $theme_name = $this->configuration['theme_name'];
+
+    // Switch to custom or default theme.
+    $this->themeManager->setCurrentTheme($theme_name);
 
     $token_options = [
       'email' => TRUE,
       'excluded_elements' => $this->configuration['excluded_elements'],
       'ignore_access' => $this->configuration['ignore_access'],
       'exclude_empty' => $this->configuration['exclude_empty'],
+      'exclude_empty_checkbox' => $this->configuration['exclude_empty_checkbox'],
       'html' => ($this->configuration['html'] && $this->supportsHtml()),
     ];
 
@@ -763,8 +894,8 @@ public function getMessage(WebformSubmissionInterface $webform_submission) {
         continue;
       }
 
-      // Determine if configuration value set to 'default'.
-      $is_default_configuration = ($configuration_value === 'default');
+      // Determine if configuration value set to '_default'.
+      $is_default_configuration = ($configuration_value === static::DEFAULT_VALUE);
       // Determine if configuration value should use global configuration.
       $is_global_configuration = in_array($configuration_key, ['reply_to', 'return_path', 'sender_mail', 'sender_name']);
       if ($is_default_configuration || (!$configuration_value && $is_global_configuration)) {
@@ -786,7 +917,7 @@ public function getMessage(WebformSubmissionInterface $webform_submission) {
         $token_options['clear'] = (strpos($configuration_key, '_mail') !== FALSE) ? TRUE : FALSE;
 
         // Get replace token values.
-        $token_value = $this->tokenManager->replace($configuration_value, $webform_submission, $token_data, $token_options);
+        $token_value = $this->replaceTokens($configuration_value, $webform_submission, $token_data, $token_options);
 
         // Decode entities for all message values except the HTML message body.
         if (!empty($token_value) && is_string($token_value) && !($token_options['html'] && $configuration_key === 'body')) {
@@ -804,8 +935,8 @@ public function getMessage(WebformSubmissionInterface $webform_submission) {
     if ($this->configuration['html'] && $this->supportsHtml()) {
       // Apply optional global format to body.
       // NOTE: $message['body'] is not passed-thru Xss::filter() to allow
-      // style tags to be supoported.
-      if ($format = $this->configFactory->get('webform.settings')->get('html_editor.format')) {
+      // style tags to be supported.
+      if ($format = $this->configFactory->get('webform.settings')->get('html_editor.mail_format')) {
         $build = [
           '#type' => 'processed_text',
           '#text' => $message['body'],
@@ -821,6 +952,9 @@ public function getMessage(WebformSubmissionInterface $webform_submission) {
     // Add webform submission.
     $message['webform_submission'] = $webform_submission;
 
+    // Add handler.
+    $message['handler'] = $this;
+
     // Switch back to active theme.
     $this->themeManager->setActiveTheme();
 
@@ -860,7 +994,7 @@ protected function getMessageEmails(WebformSubmissionInterface $webform_submissi
         $emails[] = $email_options[self::DEFAULT_OPTION];
       }
 
-      // Get submission email addresseses as an array.
+      // Get submission email addresses as an array.
       $options_element_value = $webform_submission->getElementData($element_name);
       if (is_array($options_element_value)) {
         $options_values = $options_element_value;
@@ -897,11 +1031,16 @@ protected function getMessageEmails(WebformSubmissionInterface $webform_submissi
 
     // Add user role email addresses to 'To', 'CC', and 'BCC'.
     // IMPORTANT: This is the only place where user email addresses can be
-    // used as tokens.  This prevents the webform module from being used to
-    // spam users or worse...expose user email addresses to malicious users.
+    // used as tokens. This prevents the webform module from being used to
+    // spam users or worse…expose user email addresses to malicious users.
     if (in_array($configuration_name, ['to', 'cc', 'bcc'])) {
       $roles = $this->configFactory->get('webform.settings')->get('mail.roles');
-      $emails = $this->tokenManager->replace($emails, $webform_submission, ['webform_role' => $roles]);
+      $token_data = [];
+      $token_data['webform_role'] = $roles;
+      if ($this->moduleHandler->moduleExists('webform_access')) {
+        $token_data['webform_access'] = $webform_submission;
+      }
+      $emails = $this->replaceTokens($emails, $webform_submission, $token_data);
     }
 
     // Resplit emails to make sure that emails are unique.
@@ -932,37 +1071,18 @@ protected function getMessageAttachments(WebformSubmissionInterface $webform_sub
     }
 
     $attachments = [];
-    $elements = $this->webform->getElementsInitializedAndFlattened();
-    foreach ($elements as $configuration_key => $element) {
-      $element_plugin = $this->elementManager->getElementInstance($element);
-      // Only elements that extend the 'Managed file' element can add
-      // file attachments.
-      if (!($element_plugin instanceof WebformManagedFileBase)) {
+    $elements = $this->getWebform()->getElementsInitializedAndFlattened();
+    $element_attachments = $this->getWebform()->getElementsAttachments();
+    foreach ($element_attachments as $element_attachment) {
+      // Check if the element attachment key is excluded and should not attach any files.
+      if (isset($this->configuration['excluded_elements'][$element_attachment])) {
         continue;
       }
 
-      // Check if the element is excluded and should not attach any files.
-      if (isset($this->configuration['excluded_elements'][$configuration_key])) {
-        continue;
-      }
-
-      // Get file ids.
-      $fids = $webform_submission->getElementData($configuration_key);
-      if (empty($fids)) {
-        continue;
-      }
-
-      /** @var \Drupal\file\FileInterface[] $files */
-      $files = File::loadMultiple(is_array($fids) ? $fids : [$fids]);
-      foreach ($files as $file) {
-        $attachments[] = [
-          'filecontent' => file_get_contents($file->getFileUri()),
-          'filename' => $file->getFilename(),
-          'filemime' => $file->getMimeType(),
-          // Add URL to be used by resend webform.
-          'file' => $file,
-        ];
-      }
+      $element = $elements[$element_attachment];
+      /** @var \Drupal\webform\Plugin\WebformElementAttachmentInterface $element_plugin */
+      $element_plugin = $this->elementManager->getElementInstance($element);
+      $attachments = array_merge($attachments, $element_plugin->getAttachments($element, $webform_submission));
     }
     return $attachments;
   }
@@ -980,7 +1100,7 @@ public function sendMessage(WebformSubmissionInterface $webform_submission, arra
     $message['from_name'] = preg_replace('/[<>]/', '', $message['from_name']);
 
     if (!empty($message['from_name'])) {
-      $from = $message['from_name'] . ' <' . $from . '>';
+      $from = Mail::formatDisplayName($message['from_name']) . ' <' . $from . '>';
     }
 
     $current_langcode = $this->languageManager->getCurrentLanguage()->getId();
@@ -992,7 +1112,7 @@ public function sendMessage(WebformSubmissionInterface $webform_submission, arra
           '%form' => $this->getWebform()->label(),
           '%handler' => $this->label(),
         ];
-        drupal_set_message($this->t('%form: Email not sent for %handler handler because a <em>To</em>, <em>CC</em>, or <em>BCC</em> email was not provided.', $t_args), 'warning', TRUE);
+        $this->messenger()->addWarning($this->t('%form: Email not sent for %handler handler because a <em>To</em>, <em>CC</em>, or <em>BCC</em> email was not provided.', $t_args), TRUE);
       }
       return;
     }
@@ -1006,10 +1126,11 @@ public function sendMessage(WebformSubmissionInterface $webform_submission, arra
       '#webform_submission' => $webform_submission,
       '#handler' => $this,
     ];
-    $message['body'] = trim((string) $this->themeManager->renderPlain($build));
+    $theme_name = $this->configuration['theme_name'];
+    $message['body'] = trim((string) $this->themeManager->renderPlain($build, $theme_name));
 
     if ($this->configuration['html']) {
-      switch ($this->getMailSystemSender()) {
+      switch ($this->getMailSystemFormatter()) {
         case 'swiftmailer':
           // SwiftMailer requires that the body be valid Markup.
           $message['body'] = Markup::create($message['body']);
@@ -1019,24 +1140,38 @@ public function sendMessage(WebformSubmissionInterface $webform_submission, arra
 
     // Send message.
     $key = $this->getWebform()->id() . '_' . $this->getHandlerId();
-    $this->mailManager->mail('webform', $key, $to, $current_langcode, $message, $from);
 
-    // Log message in Drupal's log.
-    $context = [
-      '@form' => $this->getWebform()->label(),
-      '@title' => $this->label(),
-      'link' => $this->getWebform()->toLink($this->t('Edit'), 'handlers')->toString(),
-    ];
-    $this->getLogger()->notice('@form webform sent @title email.', $context);
-
-    // Log message in Webform's submission log.
-    $t_args = [
-      '@from_name' => $message['from_name'],
-      '@from_mail' => $message['from_mail'],
-      '@to_mail' => $message['to_mail'],
-      '@subject' => $message['subject'],
-    ];
-    $this->log($webform_submission, 'sent email', $this->t("'@subject' sent to '@to_mail' from '@from_name' [@from_mail]'.", $t_args));
+    // Remove webform_submission and handler to prevent memory limit
+    // issues during testing.
+    if (drupal_valid_test_ua()) {
+      unset($message['webform_submission'], $message['handler']);
+    }
+
+    $result = $this->mailManager->mail('webform', $key, $to, $current_langcode, $message, $from);
+
+    if ($webform_submission->getWebform()->hasSubmissionLog()) {
+      // Log detailed message to the 'webform_submission' log.
+      $context = [
+        '@from_name' => $message['from_name'],
+        '@from_mail' => $message['from_mail'],
+        '@to_mail' => $message['to_mail'],
+        '@subject' => $message['subject'],
+        'link' => ($webform_submission->id()) ? $webform_submission->toLink($this->t('View'))->toString() : NULL,
+        'webform_submission' => $webform_submission,
+        'handler_id' => $this->getHandlerId(),
+        'operation' => 'sent email',
+      ];
+      $this->getLogger('webform_submission')->notice("'@subject' sent to '@to_mail' from '@from_name' [@from_mail]'.", $context);
+    }
+    else {
+      // Log general message to the 'webform' log.
+      $context = [
+        '@form' => $this->getWebform()->label(),
+        '@title' => $this->label(),
+        'link' => $this->getWebform()->toLink($this->t('Edit'), 'handlers')->toString(),
+      ];
+      $this->getLogger('webform')->notice('@form webform sent @title email.', $context);
+    }
 
     // Debug by displaying send email onscreen.
     if ($this->configuration['debug']) {
@@ -1046,10 +1181,12 @@ public function sendMessage(WebformSubmissionInterface $webform_submission, arra
         '%to_mail' => $message['to_mail'],
         '%subject' => $message['subject'],
       ];
-      drupal_set_message($this->t("%subject sent to %to_mail from %from_name [%from_mail].", $t_args), 'warning', TRUE);
+      $this->messenger()->addWarning($this->t("%subject sent to %to_mail from %from_name [%from_mail].", $t_args), TRUE);
       $debug_message = $this->buildDebugMessage($webform_submission, $message);
-      drupal_set_message($this->themeManager->renderPlain($debug_message, FALSE), 'warning', TRUE);
+      $this->messenger()->addWarning($this->themeManager->renderPlain($debug_message), TRUE);
     }
+
+    return $result['send'];
   }
 
   /**
@@ -1114,25 +1251,12 @@ public function resendMessageForm(array $message) {
       '#type' => 'value',
       '#value' => $message['attachments'],
     ];
-
-    // Display attached files.
     if ($message['attachments']) {
-      $file_links = [];
-      foreach ($message['attachments'] as $attachment) {
-        $file_links[] = [
-          '#theme' => 'file_link',
-          '#file' => $attachment['file'],
-          '#prefix' => '<div>',
-          '#suffix' => '</div>',
-        ];
-      }
       $element['files'] = [
         '#type' => 'item',
         '#title' => $this->t('Attachments'),
-        '#markup' => $this->themeManager->renderPlain($file_links),
-      ];
+      ] + $this->buildAttachments($message['attachments']);
     }
-
     // Preload HTML Editor and CodeMirror so that they can be properly
     // initialized when loaded via Ajax.
     $element['#attached']['library'][] = 'webform/webform.element.html_editor';
@@ -1167,13 +1291,22 @@ protected function supportsHtml() {
    *   TRUE if emailing files as attachments is supported.
    */
   protected function supportsAttachments() {
-    // If 'system.mail.interface.default' is 'test_mail_collector' allow
-    // email attachments during testing.
+    // If 'system.mail.interface.default' is 'test_mail_collector'
+    // allow email attachments during testing.
     if ($this->configFactory->get('system.mail')->get('interface.default') == 'test_mail_collector') {
       return TRUE;
     }
 
-    return $this->moduleHandler->moduleExists('mailsystem');
+    // If webform_test.module is installed and this is a test webform
+    // allow email attachments.
+    if (strpos($this->getWebform()->id(), 'test_') === 0
+      && $this->moduleHandler->moduleExists('webform_test')) {
+      return TRUE;
+    }
+
+    // The Mail System module, which supports a variety of mail handlers,
+    // and the SMTP module support attachments.
+    return $this->moduleHandler->moduleExists('mailsystem') || $this->moduleHandler->moduleExists('smtp');
   }
 
   /**
@@ -1223,29 +1356,38 @@ protected function buildDebugMessage(WebformSubmissionInterface $webform_submiss
     $build['body'] = [
       '#type' => 'item',
       '#title' => $this->t('Body'),
-      '#markup' => ($message['html']) ? $message['body'] : '<pre>' . htmlentities($message['body']) . '</pre>',
-      '#allowed_tags' => Xss::getAdminTagList(),
+      '#markup' => Markup::create('<pre>' . htmlentities($message['body']) . '</pre>'),
       '#wrapper_attributes' => ['style' => 'margin: 0'],
     ];
+    // Attachments.
+    if (!empty($message['attachments'])) {
+      $build['attachments_divider'] = ['#markup' => '<hr />'];
+      $build['attachments'] = [
+        '#type' => 'item',
+        '#title' => $this->t('Attachments'),
+        '#wrapper_attributes' => ['style' => 'margin: 0'],
+        'files' => $this->buildAttachments($message['attachments']),
+      ];
+    }
     return $build;
   }
 
   /**
-   * Get the Mail System's sender module name.
+   * Get the Mail System's formatter module name.
    *
    * @return string
-   *   The Mail System's sender module name.
+   *   The Mail System's formatter module name.
    */
-  protected function getMailSystemSender() {
+  protected function getMailSystemFormatter() {
     $mailsystem_config = $this->configFactory->get('mailsystem.settings');
-    // Get the default sender.
-    $mailsystem_sender = $mailsystem_config->get('defaults.sender');
+    // Get the default formatter.
+    $mailsystem_formatter = $mailsystem_config->get('defaults.formatter');
     // Look for a global setting for the webform module.
-    $mailsystem_sender = $mailsystem_config->get('modules.webform.none.sender') ?: $mailsystem_sender;
+    $mailsystem_formatter = $mailsystem_config->get('modules.webform.none.formatter') ?: $mailsystem_formatter;
     // Look for a specific setting for this webform module's email.
     $key = 'email_' . $this->getHandlerId();
-    $mailsystem_sender = $mailsystem_config->get("modules.webform.$key.sender") ?: $mailsystem_sender;
-    return $mailsystem_sender;
+    $mailsystem_formatter = $mailsystem_config->get("modules.webform.$key.formatter") ?: $mailsystem_formatter;
+    return $mailsystem_formatter;
   }
 
   /**
@@ -1270,7 +1412,7 @@ protected function getBodyDefaultValues($format = NULL) {
   }
 
   /**
-   * Build A select other element for email addresss and names.
+   * Build A select other element for email address and names.
    *
    * @param string $name
    *   The element's key.
@@ -1278,25 +1420,29 @@ protected function getBodyDefaultValues($format = NULL) {
    *   The element's title.
    * @param string $label
    *   The element's label.
+   * @param bool $required
+   *   TRUE if the element is required.
    * @param array $element_options
    *   The element options.
    * @param array $options_options
    *   The options options.
    * @param array $role_options
    *   The (user) role options.
-   * @param bool $required
-   *   TRUE if the element is required.
+   * @param array $other_options
+   *   The other options.
    *
    * @return array
    *   A select other element.
    */
-  protected function buildElement($name, $title, $label, array $element_options, array $options_options = NULL, array $role_options = NULL, $required = FALSE) {
+  protected function buildElement($name, $title, $label, $required = FALSE, array $element_options, array $options_options = NULL, array $role_options = NULL, array $other_options = NULL) {
     list($element_name, $element_type) = (strpos($name, '_') !== FALSE) ? explode('_', $name) : [$name, 'text'];
 
+    $default_option = $this->getDefaultConfigurationValue($name);
+
     $options = [];
-    $options[WebformSelectOther::OTHER_OPTION] = $this->t('Custom @label...', ['@label' => $label]);
-    if ($default_option = $this->getDefaultConfigurationValue($name)) {
-      $options[(string) $this->t('Default')] = ['default' => $default_option];
+    $options[WebformSelectOther::OTHER_OPTION] = $this->t('Custom @label…', ['@label' => $label]);
+    if ($default_option) {
+      $options[(string) $this->t('Default')] = [static::DEFAULT_VALUE => $default_option];
     }
     if ($element_options) {
       $options[(string) $this->t('Elements')] = $element_options;
@@ -1307,6 +1453,9 @@ protected function buildElement($name, $title, $label, array $element_options, a
     if ($role_options) {
       $options[(string) $this->t('Roles')] = $role_options;
     }
+    if ($other_options) {
+      $options[(string) $this->t('Other')] = $other_options;
+    }
 
     $ajax_wrapper = Html::getUniqueId('ajax-wrapper');
 
@@ -1321,12 +1470,11 @@ protected function buildElement($name, $title, $label, array $element_options, a
       '#options' => $options,
       '#empty_option' => (!$required) ? $this->t('- None -') : NULL,
       '#other__title' => $title,
-      '#other__title_display' => 'hidden',
-      '#other__placeholder' => $this->t('Enter @label...', ['@label' => $label]),
+      '#other__title_display' => 'invisible',
+      '#other__placeholder' => $this->t('Enter @label…', ['@label' => $label]),
       '#other__type' => ($element_type == 'mail') ? 'webform_email_multiple' : 'textfield',
       '#other__allow_tokens' => TRUE,
       '#required' => $required,
-      '#parents' => ['settings', $name],
       '#default_value' => $this->configuration[$name],
     ];
 
@@ -1383,18 +1531,19 @@ protected function buildElement($name, $title, $label, array $element_options, a
       '#value' => $this->t('Update'),
       '#name' => $element_name . '_update_button',
       '#validate' => [],
-      '#limit_validation_errors' => [$element[$name]['#parents']],
+      '#limit_validation_errors' => [['settings', $name]],
       '#submit' => [[get_called_class(), 'rebuildCallback']],
       '#ajax' => [
         'callback' => [get_called_class(), 'ajaxCallback'],
         'wrapper' => $ajax_wrapper,
         'progress' => ['type' => 'fullscreen'],
       ],
+      // Disable validation, hide button, add submit button trigger class.
       '#attributes' => [
+        'formnovalidate' => 'formnovalidate',
         'class' => [
           'js-hide',
           "js-$ajax_wrapper-submit",
-          'js-webform-novalidate',
         ],
       ],
     ];
@@ -1423,10 +1572,10 @@ protected function buildElement($name, $title, $label, array $element_options, a
       $mapping_options[self::DEFAULT_OPTION] = $this->t('Default (This email address will always be included)');
 
       // Set placeholder emails.
-      $destination_placeholde_emails = ['example@example.com', '[site:mail]'];
+      $destination_placeholder_emails = ['example@example.com', '[site:mail]'];
       if ($role_options) {
         $role_names = array_keys($role_options);
-        $destination_placeholde_emails[] = ($role_names[0] === '[webform_role:authenticated]' && isset($role_names[1])) ? $role_names[1] : $role_names[0];
+        $destination_placeholder_emails[] = ($role_names[0] === '[webform_role:authenticated]' && isset($role_names[1])) ? $role_names[1] : $role_names[0];
       }
       $element[$options_name] = [
         '#type' => 'webform_mapping',
@@ -1434,7 +1583,6 @@ protected function buildElement($name, $title, $label, array $element_options, a
         '#description' => $this->t('The selected element has multiple options. You may enter email addresses for each choice. When that choice is selected, an email will be sent to the corresponding addresses. If a field is left blank, no email will be sent for that option. You may use tokens.') . '<br /><br />',
         '#description_display' => 'before',
         '#required' => TRUE,
-        '#parents' => ['settings', $options_name],
         '#default_value' => WebformOptionsHelper::decodeConfig($this->configuration[$options_name]),
 
         '#source' => $mapping_options,
@@ -1444,20 +1592,47 @@ protected function buildElement($name, $title, $label, array $element_options, a
         '#destination__allow_tokens' => TRUE,
         '#destination__title' => $this->t('Email addresses'),
         '#destination__description' => NULL,
-        '#destination__placeholder' => implode(', ', $destination_placeholde_emails),
+        '#destination__placeholder' => implode(', ', $destination_placeholder_emails),
       ];
     }
     else {
       $element[$options_name] = [
         '#type' => 'value',
         '#value' => [],
-        '#parents' => ['settings', $options_name],
       ];
     }
 
     return $element;
   }
 
+  /**
+   * Build attachment to be displayed via debug message and resend form.
+   *
+   * @param array $attachments
+   *   An array of email attachments.
+   *
+   * @return array
+   *   A renderable array containing links to attachments.
+   */
+  protected function buildAttachments(array $attachments) {
+    $build = [];
+    foreach ($attachments as $attachment) {
+      $t_args = [
+        '@filename' => $attachment['filename'],
+        '@filemime' => $attachment['filemime'],
+        '@filesize' => format_size(mb_strlen($attachment['filecontent'])),
+      ];
+      if (!empty($attachment['_uri'])) {
+        $t_args[':href'] = $attachment['_uri'];
+        $build[] = ['#markup' => $this->t('<strong><a href=":href">@filename</a></strong> (@filemime) - @filesize ', $t_args)];
+      }
+      else {
+        $build[] = ['#markup' => $this->t('<strong>@filename</strong> (@filemime) - @filesize ', $t_args)];
+      }
+    }
+    return $build;
+  }
+
   /**
    * Rebuild callback.
    *
@@ -1506,4 +1681,12 @@ protected function getElementKeyFromToken($token, $format = 'raw') {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildTokenTreeElement(array $token_types = [], $description = NULL) {
+    $description = $description ?: $this->t('Use [webform_submission:values:ELEMENT_KEY:raw] to get plain text values.');
+    return parent::buildTokenTreeElement($token_types, $description);
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformHandler/RemotePostWebformHandler.php b/web/modules/webform/src/Plugin/WebformHandler/RemotePostWebformHandler.php
index a315e8f1fdd97a4292b02ee0861c7aa07d7d946d..524bfc0fb2372bf20bcef9c8e8ba6dd68a319744 100644
--- a/web/modules/webform/src/Plugin/WebformHandler/RemotePostWebformHandler.php
+++ b/web/modules/webform/src/Plugin/WebformHandler/RemotePostWebformHandler.php
@@ -35,6 +35,7 @@
  *   cardinality = \Drupal\webform\Plugin\WebformHandlerInterface::CARDINALITY_UNLIMITED,
  *   results = \Drupal\webform\Plugin\WebformHandlerInterface::RESULTS_PROCESSED,
  *   submission = \Drupal\webform\Plugin\WebformHandlerInterface::SUBMISSION_OPTIONAL,
+ *   tokens = TRUE,
  * )
  */
 class RemotePostWebformHandler extends WebformHandlerBase {
@@ -74,6 +75,17 @@ class RemotePostWebformHandler extends WebformHandlerBase {
    */
   protected $elementManager;
 
+  /**
+   * List of unsupported webform submission properties.
+   *
+   * The below properties will not being included in a remote post.
+   *
+   * @var array
+   */
+  protected $unsupportedProperties = [
+    'metatag',
+  ];
+
   /**
    * {@inheritdoc}
    */
@@ -111,18 +123,21 @@ public static function create(ContainerInterface $container, array $configuratio
    */
   public function getSummary() {
     $configuration = $this->getConfiguration();
+    $settings = $configuration['settings'];
+
     if (!$this->isResultsEnabled()) {
-      $configuration['settings']['updated_url'] = '';
-      $configuration['settings']['deleted_url'] = '';
+      $settings['updated_url'] = '';
+      $settings['deleted_url'] = '';
     }
     if (!$this->isDraftEnabled()) {
-      $configuration['settings']['draft_url'] = '';
+      $settings['draft_url'] = '';
     }
     if (!$this->isConvertEnabled()) {
-      $configuration['settings']['converted_url'] = '';
+      $settings['converted_url'] = '';
     }
+
     return [
-      '#settings' => $configuration['settings'],
+      '#settings' => $settings,
     ] + parent::getSummary();
   }
 
@@ -215,7 +230,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
         '#title' => $this->t('@title URL', $t_args),
         '#description' => $this->t('The full URL to POST to when an existing webform submission is @state. (e.g. @url)', $t_args),
         '#required' => ($state === WebformSubmissionInterface::STATE_COMPLETED),
-        '#parents' => ['settings', $state_url],
         '#default_value' => $this->configuration[$state_url],
       ];
       $form[$state][$state_custom_data] = [
@@ -223,7 +237,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
         '#mode' => 'yaml',
         '#title' => $this->t('@title custom data', $t_args),
         '#description' => $this->t('Enter custom data that will be included when a webform submission is @state.', $t_args),
-        '#parents' => ['settings', $state_custom_data],
         '#states' => ['visible' => [':input[name="settings[' . $state_url . ']"]' => ['filled' => TRUE]]],
         '#default_value' => $this->configuration[$state_custom_data],
       ];
@@ -248,23 +261,22 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#required' => TRUE,
       '#options' => [
         'POST' => 'POST',
+        'PUT' => 'PUT',
         'GET' => 'GET',
       ],
-      '#parents' => ['settings', 'method'],
       '#default_value' => $this->configuration['method'],
     ];
     $form['additional']['type'] = [
       '#type' => 'select',
       '#title' => $this->t('Post type'),
-      '#description' => $this->t('Use x-www-form-urlencoded if unsure, as it is the default format for HTML webforms. You also have the option to post data in <a href="http://www.json.org/" target="_blank">JSON</a> format.'),
+      '#description' => $this->t('Use x-www-form-urlencoded if unsure, as it is the default format for HTML webforms. You also have the option to post data in <a href="http://www.json.org/">JSON</a> format.'),
       '#options' => [
         'x-www-form-urlencoded' => $this->t('x-www-form-urlencoded'),
         'json' => $this->t('JSON'),
       ],
-      '#parents' => ['settings', 'type'],
       '#states' => [
-        'visible' => [':input[name="settings[method]"]' => ['value' => 'POST']],
-        'required' => [':input[name="settings[method]"]' => ['value' => 'POST']],
+        '!visible' => [':input[name="settings[method]"]' => ['value' => 'GET']],
+        '!required' => [':input[name="settings[method]"]' => ['value' => 'GET']],
       ],
       '#default_value' => $this->configuration['type'],
     ];
@@ -273,22 +285,19 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#mode' => 'yaml',
       '#title' => $this->t('Custom data'),
       '#description' => $this->t('Enter custom data that will be included in all remote post requests.'),
-      '#parents' => ['settings', 'custom_data'],
       '#default_value' => $this->configuration['custom_data'],
     ];
     $form['additional']['custom_options'] = [
       '#type' => 'webform_codemirror',
       '#mode' => 'yaml',
       '#title' => $this->t('Custom options'),
-      '#description' => $this->t('Enter custom <a href=":href">request options</a> that will be used by the Guzzle HTTP client. Request options can included custom headers.', [':href' => 'http://docs.guzzlephp.org/en/stable/request-options.html']),
-      '#parents' => ['settings', 'custom_options'],
+      '#description' => $this->t('Enter custom <a href=":href">request options</a> that will be used by the Guzzle HTTP client. Request options can include custom headers.', [':href' => 'http://docs.guzzlephp.org/en/stable/request-options.html']),
       '#default_value' => $this->configuration['custom_options'],
     ];
     $form['additional']['message'] = [
       '#type' => 'webform_html_editor',
       '#title' => $this->t('Custom error response message'),
       '#description' => $this->t('This message is displayed when the response status code is not 2xx'),
-      '#parents' => ['settings', 'message'],
       '#default_value' => $this->configuration['message'],
     ];
     $form['additional']['messages_token'] = [
@@ -299,8 +308,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
     $form['additional']['messages'] = [
       '#type' => 'webform_multiple',
       '#title' => $this->t('Custom error response messages'),
-      '#description' => $this->t('Enter custom response messages for specific status codes.') . '<br/>' . $this->t('Defaults to: %value', ['%value' => $this->messageManager->render(WebformMessageManagerInterface::SUBMISSION_EXCEPTION)]),
+      '#description' => $this->t('Enter custom response messages for specific status codes.') . '<br/>' . $this->t('Defaults to: %value', ['%value' => $this->messageManager->render(WebformMessageManagerInterface::SUBMISSION_EXCEPTION_MESSAGE)]),
       '#empty_items' => 0,
+      '#no_items_message' => $this->t('No error response messages entered. Please add messages below.'),
       '#add' => FALSE,
       '#element' => [
         'code' => [
@@ -324,7 +334,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
           '#title' => $this->t('Response message'),
         ],
       ],
-      '#parents' => ['settings', 'messages'],
       '#default_value' => $this->configuration['messages'],
     ];
 
@@ -338,7 +347,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#title' => $this->t('Enable debugging'),
       '#description' => $this->t('If checked, posted submissions will be displayed onscreen to all users.'),
       '#return_value' => TRUE,
-      '#parents' => ['settings', 'debug'],
       '#default_value' => $this->configuration['debug'],
     ];
 
@@ -364,15 +372,12 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#title_display' => 'invisible',
       '#webform_id' => $webform->id(),
       '#required' => TRUE,
-      '#parents' => ['settings', 'excluded_data'],
       '#default_value' => $this->configuration['excluded_data'],
     ];
 
-    $form['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $this->elementTokenValidate($form);
 
-    $this->tokenManager->elementValidate($form);
-
-    return $form;
+    return $this->setSettingsParents($form);
   }
 
   /**
@@ -422,9 +427,13 @@ protected function remotePost($state, WebformSubmissionInterface $webform_submis
     $this->messageManager->setWebformSubmission($webform_submission);
 
     $request_url = $this->configuration[$state . '_url'];
+    $request_url = $this->replaceTokens($request_url, $webform_submission);
     $request_method = (!empty($this->configuration['method'])) ? $this->configuration['method'] : 'POST';
-    $request_type = ($request_method == 'POST') ? $this->configuration['type'] : NULL;
+    $request_type = ($request_method !== 'GET') ? $this->configuration['type'] : NULL;
+
+    // Get request options with tokens replaced.
     $request_options = (!empty($this->configuration['custom_options'])) ? Yaml::decode($this->configuration['custom_options']) : [];
+    $request_options = $this->replaceTokens($request_options, $webform_submission);
 
     try {
       if ($request_method === 'GET') {
@@ -434,8 +443,9 @@ protected function remotePost($state, WebformSubmissionInterface $webform_submis
         $response = $this->httpClient->get($request_url, $request_options);
       }
       else {
+        $method = strtolower($request_method);
         $request_options[($request_type == 'json' ? 'json' : 'form_params')] = $this->getRequestData($state, $webform_submission);
-        $response = $this->httpClient->post($request_url, $request_options);
+        $response = $this->httpClient->$method($request_url, $request_options);
       }
     }
     catch (RequestException $request_exception) {
@@ -467,7 +477,7 @@ protected function remotePost($state, WebformSubmissionInterface $webform_submis
     if ($submission_has_token) {
       $response_data = $this->getResponseData($response);
       $token_data = ['webform_handler' => [$this->getHandlerId() => [$state => $response_data]]];
-      $submission_data = $this->tokenManager->replace($submission_data, $webform_submission, $token_data);
+      $submission_data = $this->replaceTokens($submission_data, $webform_submission, $token_data);
       $webform_submission->setData($submission_data);
       // Resave changes to the submission data without invoking any hooks
       // or handlers.
@@ -494,10 +504,16 @@ protected function getRequestData($state, WebformSubmissionInterface $webform_su
     // Get submission and elements data.
     $data = $webform_submission->toArray(TRUE);
 
-    // Flatten data.
-    // Prioritizing elements before the submissions fields.
-    $data = $data['data'] + $data;
+    // Remove unsupported properties from data.
+    // These are typically added by other module's like metatag.
+    $unsupported_properties = array_combine($this->unsupportedProperties, $this->unsupportedProperties);
+    $data = array_diff_key($data, $unsupported_properties);
+
+    // Flatten data and prioritize the element data over the
+    // webform submission data.
+    $element_data = $data['data'];
     unset($data['data']);
+    $data = $element_data + $data;
 
     // Excluded selected submission data.
     $data = array_diff_key($data, $this->configuration['excluded_data']);
@@ -505,6 +521,10 @@ protected function getRequestData($state, WebformSubmissionInterface $webform_su
     // Append uploaded file name, uri, and base64 data to data.
     $webform = $this->getWebform();
     foreach ($data as $element_key => $element_value) {
+      if (empty($element_value)) {
+        continue;
+      }
+
       $element = $webform->getElement($element_key);
       if (!$element) {
         continue;
@@ -515,15 +535,17 @@ protected function getRequestData($state, WebformSubmissionInterface $webform_su
         continue;
       }
 
-      /** @var \Drupal\file\FileInterface $file */
-      $file = File::load($element_value);
-      if (!$file) {
-        continue;
+      if ($element_plugin->hasMultipleValues($element)) {
+        foreach ($element_value as $fid) {
+          $data['_' . $element_key][] = $this->getResponseFileData($fid);
+        }
+      }
+      else {
+        $data['_' . $element_key] = $this->getResponseFileData($element_value);
+        // @deprecated in Webform 8.x-5.0-rc17. Use new format
+        // The code needs to be removed before 8.x-5.0 or 8.x-6.x.
+        $data += $this->getResponseFileData($element_value, $element_key . '__');
       }
-
-      $data[$element_key .'__name'] = $file->getFilename();
-      $data[$element_key .'__uri'] = $file->getFileUri();
-      $data[$element_key .'__data'] = base64_encode(file_get_contents($file->getFileUri()));
     }
 
     // Append custom data.
@@ -537,11 +559,39 @@ protected function getRequestData($state, WebformSubmissionInterface $webform_su
     }
 
     // Replace tokens.
-    $data = $this->tokenManager->replace($data, $webform_submission);
+    $data = $this->replaceTokens($data, $webform_submission);
 
     return $data;
   }
 
+  /**
+   * Get response file data.
+   *
+   * @param int $fid
+   *   A file id.
+   * @param string|null $prefix
+   *   A prefix to prepended to data.
+   *
+   * @return array
+   *   An associative array containing file data (name, uri, mime, and data).
+   */
+  protected function getResponseFileData($fid, $prefix = '') {
+    /** @var \Drupal\file\FileInterface $file */
+    $file = File::load($fid);
+    if (!$file) {
+      return [];
+    }
+
+    $data = [];
+    $data[$prefix . 'id'] = (int) $file->id();
+    $data[$prefix . 'name'] = $file->getFilename();
+    $data[$prefix . 'uri'] = $file->getFileUri();
+    $data[$prefix . 'mime'] = $file->getMimeType();
+    $data[$prefix . 'uuid'] = $file->uuid();
+    $data[$prefix . 'data'] = base64_encode(file_get_contents($file->getFileUri()));
+    return $data;
+  }
+
   /**
    * Get response data.
    *
@@ -602,10 +652,10 @@ protected function isDraftEnabled() {
   }
 
   /**
-   * Determine if converting anoynmous submissions to authenticated is enabled.
+   * Determine if converting anonymous submissions to authenticated is enabled.
    *
    * @return bool
-   *   TRUE if converting anoynmous submissions to authenticated is enabled.
+   *   TRUE if converting anonymous submissions to authenticated is enabled.
    */
   protected function isConvertEnabled() {
     return $this->isDraftEnabled() && ($this->getWebform()->getSetting('form_convert_anonymous') === TRUE);
@@ -761,7 +811,7 @@ protected function debug($message, $state, $request_url, $request_method, $reque
       '#markup' => $message,
     ];
 
-    drupal_set_message(\Drupal::service('renderer')->renderPlain($build), $type);
+    $this->messenger()->addMessage(\Drupal::service('renderer')->renderPlain($build), $type);
   }
 
   /**
@@ -810,12 +860,12 @@ protected function handleError($state, $message, $request_url, $request_method,
         ],
       ];
       $build_message = [
-        '#markup' => $this->tokenManager->replace($custom_response_message, $this->getWebform(), $token_data)
+        '#markup' => $this->replaceTokens($custom_response_message, $this->getWebform(), $token_data),
       ];
-      drupal_set_message(\Drupal::service('renderer')->renderPlain($build_message), 'error');
+      $this->messenger()->addError(\Drupal::service('renderer')->renderPlain($build_message));
     }
     else {
-      $this->messageManager->display(WebformMessageManagerInterface::SUBMISSION_EXCEPTION, 'error');
+      $this->messageManager->display(WebformMessageManagerInterface::SUBMISSION_EXCEPTION_MESSAGE, 'error');
     }
   }
 
@@ -829,13 +879,23 @@ protected function handleError($state, $message, $request_url, $request_method,
    *   A custom custom response message.
    */
   protected function getCustomResponseMessage($response) {
-    $status_code = $response->getStatusCode();
-    foreach ($this->configuration['messages'] as $message_item) {
-      if ($message_item['code'] == $status_code) {
-        return $message_item['message'];
+    if ($response instanceof ResponseInterface) {
+      $status_code = $response->getStatusCode();
+      foreach ($this->configuration['messages'] as $message_item) {
+        if ($message_item['code'] == $status_code) {
+          return $message_item['message'];
+        }
       }
     }
     return (!empty($this->configuration['message'])) ? $this->configuration['message'] : '';
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildTokenTreeElement(array $token_types = [], $description = NULL) {
+    $description = $description ?: $this->t('Use [webform_submission:values:ELEMENT_KEY:raw] to get plain text values.');
+    return parent::buildTokenTreeElement($token_types, $description);
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformHandler/SettingsWebformHandler.php b/web/modules/webform/src/Plugin/WebformHandler/SettingsWebformHandler.php
index 11cf72ea23343c9444b71d115223829af587fbb7..40cb28ea532c55ba94b0f53de8b0e84637a3786e 100644
--- a/web/modules/webform/src/Plugin/WebformHandler/SettingsWebformHandler.php
+++ b/web/modules/webform/src/Plugin/WebformHandler/SettingsWebformHandler.php
@@ -27,6 +27,7 @@
  *   cardinality = \Drupal\webform\Plugin\WebformHandlerInterface::CARDINALITY_UNLIMITED,
  *   results = \Drupal\webform\Plugin\WebformHandlerInterface::RESULTS_PROCESSED,
  *   submission = \Drupal\webform\Plugin\WebformHandlerInterface::SUBMISSION_OPTIONAL,
+ *   tokens = TRUE,
  * )
  */
 class SettingsWebformHandler extends WebformHandlerBase {
@@ -75,16 +76,20 @@ public static function create(ContainerInterface $container, array $configuratio
    * {@inheritdoc}
    */
   public function getSummary() {
+    $configuration = $this->getConfiguration();
+    $settings = $configuration['settings'];
+
     $setting_definitions = $this->getSettingsDefinitions();
-    $settings = $this->getSettingsOverride();
-    foreach ($settings as $name => $value) {
-      $settings[$name] = [
+    $setting_override = $this->getSettingsOverride();
+    foreach ($setting_override as $name => $value) {
+      $settings['settings'][$name] = [
         'title' => $setting_definitions[$name]['label'],
         'value' => $value,
       ];
     }
+
     return [
-      '#settings' => $this->configuration + ['settings' => $settings],
+      '#settings' => $settings,
     ] + parent::getSummary();
   }
 
@@ -125,7 +130,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#description' => $this->t('A message to be displayed on the preview page.'),
       '#default_value' => $this->configuration['preview_message'],
     ];
-    $form['preview_settings']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['preview_settings']['token_tree_link'] = $this->buildTokenTreeElement();
 
     // Confirmation settings.
     $confirmation_type = $this->getWebform()->getSetting('confirmation_type');
@@ -159,7 +164,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#default_value' => $this->configuration['confirmation_message'],
       '#access' => !empty($this->configuration['confirmation_message']) || $has_confirmation_message,
     ];
-    $form['confirmation_settings']['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['confirmation_settings']['token_tree_link'] = $this->buildTokenTreeElement();
 
     // Custom settings.
     $custom_settings = $this->configuration;
@@ -176,6 +181,8 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#title' => $this->t('Custom settings (YAML)'),
       '#description' => $this->t('Enter the setting name and value as YAML.'),
       '#default_value' => $custom_settings,
+      // Must set #parents because custom is not a configuration value.
+      // @see \Drupal\webform\Plugin\WebformHandler\SettingsWebformHandler::submitConfigurationForm
       '#parents' => ['settings', 'custom'],
     ];
 
@@ -221,9 +228,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#default_value' => $this->configuration['debug'],
     ];
 
-    $this->tokenManager->elementValidate($form);
+    $this->elementTokenValidate($form);
 
-    return $this->setSettingsParentsRecursively($form);
+    return $this->setSettingsParents($form);
   }
 
   /**
@@ -248,10 +255,10 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
   public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
     // Completely reset configuration so that custom configuration will always
     // be reset.
-    $this->configuration = [];
+    $this->configuration = $this->defaultConfiguration();
 
     parent::submitConfigurationForm($form, $form_state);
-    parent::applyFormStateToConfiguration($form_state);
+    $this->applyFormStateToConfiguration($form_state);
 
     // Remove all empty strings from preview and confirmation settings.
     $this->configuration = array_filter($this->configuration);
@@ -331,7 +338,7 @@ protected function displayDebug(WebformSubmissionInterface $webform_submission)
       '#header' => $header,
       '#rows' => $rows,
     ];
-    drupal_set_message(\Drupal::service('renderer')->renderPlain($build), 'warning', FALSE);
+    $this->messenger()->addWarning(\Drupal::service('renderer')->renderPlain($build));
   }
 
   /****************************************************************************/
@@ -389,7 +396,7 @@ protected function getSubmissionSettingsOverride(WebformSubmissionInterface $web
       // Replace token value and cast booleans and integers.
       $type = $settings_definitions[$name]['type'];
       if (in_array($type, ['boolean', 'integer'])) {
-        $value = $this->tokenManager->replace($value, $webform_submission);
+        $value = $this->replaceTokens($value, $webform_submission);
         settype($value, $type);
         $settings_override[$name] = $value;
       }
diff --git a/web/modules/webform/src/Plugin/WebformHandlerBase.php b/web/modules/webform/src/Plugin/WebformHandlerBase.php
index 1da7c877e0ce2d6f08160ab9124e7ec7bc1f9a8c..e098270fd09f44ac5c3c3bfaec100d91511720e9 100644
--- a/web/modules/webform/src/Plugin/WebformHandlerBase.php
+++ b/web/modules/webform/src/Plugin/WebformHandlerBase.php
@@ -3,11 +3,13 @@
 namespace Drupal\webform\Plugin;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Plugin\PluginBase;
-use Drupal\Core\Render\Element;
+use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformSubmissionConditionsValidatorInterface;
 use Drupal\webform\WebformSubmissionInterface;
@@ -30,6 +32,13 @@ abstract class WebformHandlerBase extends PluginBase implements WebformHandlerIn
    */
   protected $webform = NULL;
 
+  /**
+   * The webform submission.
+   *
+   * @var \Drupal\webform\WebformSubmissionInterface
+   */
+  protected $webformSubmission = NULL;
+
   /**
    * The webform handler ID.
    *
@@ -93,6 +102,13 @@ abstract class WebformHandlerBase extends PluginBase implements WebformHandlerIn
    */
   protected $conditionsValidator;
 
+  /**
+   * The webform token manager.
+   *
+   * @var \Drupal\webform\WebformTokenManagerInterface
+   */
+  protected $tokenManager;
+
   /**
    * Constructs a WebformHandlerBase object.
    *
@@ -101,7 +117,7 @@ abstract class WebformHandlerBase extends PluginBase implements WebformHandlerIn
    * webform. Make sure not include any services as a dependency injection
    * that directly connect to the database. This will prevent
    * "LogicException: The database connection is not serializable." exceptions
-   * from being thrown when a form is serialized via an Ajax callaback and/or
+   * from being thrown when a form is serialized via an Ajax callback and/or
    * form build.
    *
    * @param array $configuration
@@ -128,6 +144,10 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
     $this->configFactory = $config_factory;
     $this->submissionStorage = $entity_type_manager->getStorage('webform_submission');
     $this->conditionsValidator = $conditions_validator;
+
+    // @todo Webform 8.x-6.x: Properly inject the token manager.
+    // @todo Webform 8.x-6.x: Update handlers that injects the token manager.
+    $this->tokenManager = \Drupal::service('webform.token_manager');
   }
 
   /**
@@ -160,6 +180,21 @@ public function getWebform() {
     return $this->webform;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function setWebformSubmission(WebformSubmissionInterface $webform_submission = NULL) {
+    $this->webformSubmission = $webform_submission;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWebformSubmission() {
+    return $this->webformSubmission;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -199,6 +234,13 @@ public function supportsConditions() {
     return $this->pluginDefinition['conditions'];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function supportsTokens() {
+    return $this->pluginDefinition['tokens'];
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -274,11 +316,26 @@ public function getWeight() {
     return $this->weight;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function enable() {
+    return $this->setStatus(TRUE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function disable() {
+    return $this->setStatus(FALSE);
+  }
+
   /**
    * {@inheritdoc}
    */
   public function isExcluded() {
-    return $this->configFactory->get('webform.settings')->get('handler.excluded_handlers.' . $this->pluginDefinition['id']) ? TRUE : FALSE;
+    return $this->configFactory->get('webform.settings')
+      ->get('handler.excluded_handlers.' . $this->pluginDefinition['id']) ? TRUE : FALSE;
   }
 
   /**
@@ -325,8 +382,14 @@ public function checkConditions(WebformSubmissionInterface $webform_submission)
       return TRUE;
     }
 
+    // Get conditions.
     $state = key($conditions);
     $conditions = $conditions[$state];
+
+    // Replace tokens in conditions.
+    $conditions = $this->replaceTokens($conditions, $webform_submission);
+
+    // Validation conditions.
     $result = $this->conditionsValidator->validateConditions($conditions, $webform_submission);
 
     // Negate result for 'disabled' state.
@@ -555,7 +618,24 @@ public function deleteElement($key, array $element) {}
    *
    * This helper method looks looks for the handler default configuration keys
    * within a form and set a matching element's #parents property to
-   * ['settings', '{element_kye}']
+   * ['settings', '{element_key}']
+   *
+   * @param array $elements
+   *   An array of form elements.
+   *
+   * @return array
+   *   Form element with #parents set.
+   */
+  protected function setSettingsParents(array &$elements) {
+    return $this->setSettingsParentsRecursively($elements);
+  }
+
+  /**
+   * Set configuration settings parents.
+   *
+   * This helper method looks looks for the handler default configuration keys
+   * within a form and set a matching element's #parents property to
+   * ['settings', '{element_key}']
    *
    * @param array $elements
    *   An array of form elements.
@@ -567,7 +647,7 @@ protected function setSettingsParentsRecursively(array &$elements) {
     $default_configuration = $this->defaultConfiguration();
     foreach ($elements as $element_key => &$element) {
       // Only a form element can have #parents.
-      if (Element::property($element_key) || !is_array($element)) {
+      if (!WebformElementHelper::isElement($element, $element_key)) {
         continue;
       }
 
@@ -577,7 +657,14 @@ protected function setSettingsParentsRecursively(array &$elements) {
         continue;
       }
 
-      if (array_key_exists($element_key, $default_configuration) && isset($element['#type'])) {
+      // Only set #parents when #element has…
+      // - Default configuration.
+      // - Is an input.
+      // - #default_value or #value (aka input).
+      // - Not a container with children.
+      if (array_key_exists($element_key, $default_configuration)
+        && isset($element['#type'])
+        && !WebformElementHelper::hasChildren($element)) {
         $element['#parents'] = ['settings', $element_key];
       }
       else {
@@ -587,18 +674,80 @@ protected function setSettingsParentsRecursively(array &$elements) {
     return $elements;
   }
 
+  /****************************************************************************/
+  // Token methods.
+  /****************************************************************************/
+
+  /**
+   * Replace tokens in text with no render context.
+   *
+   * @param string|array $text
+   *   A string of text that may contain tokens.
+   * @param \Drupal\Core\Entity\EntityInterface|null $entity
+   *   A Webform or Webform submission entity.
+   * @param array $data
+   *   (optional) An array of keyed objects.
+   * @param array $options
+   *   (optional) A keyed array of settings and flags to control the token
+   *   replacement process. Supported options are:
+   *   - langcode: A language code to be used when generating locale-sensitive
+   *     tokens.
+   *   - callback: A callback function that will be used to post-process the
+   *     array of token replacements after they are generated.
+   *   - clear: A boolean flag indicating that tokens should be removed from the
+   *     final text if no replacement value can be generated.
+   *
+   * @return string|array
+   *   Text or array with tokens replaced.
+   */
+  protected function replaceTokens($text, EntityInterface $entity = NULL, array $data = [], array $options = []) {
+    return $this->tokenManager->replaceNoRenderContext($text, $entity, $data, $options);
+  }
+
+  /**
+   * Build token tree element.
+   *
+   * @param array $token_types
+   *   (optional) An array containing token types that should be shown in the tree.
+   * @param string $description
+   *   (optional) Description to appear after the token tree link.
+   *
+   * @return array
+   *   A render array containing a token tree link wrapped in a div.
+   */
+  protected function buildTokenTreeElement(array $token_types = [], $description = NULL) {
+    return $this->tokenManager->buildTreeElement($token_types, $description);
+  }
+
+  /**
+   * Validate form that should have tokens in it.
+   *
+   * @param array $form
+   *   A form.
+   * @param array $token_types
+   *   An array containing token types that should be validated.
+   *
+   * @see token_element_validate()
+   */
+  protected function elementTokenValidate(array &$form, array $token_types = ['webform', 'webform_submission', 'webform_handler']) {
+    return $this->tokenManager->elementValidate($form, $token_types);
+  }
+
   /****************************************************************************/
   // Logging methods.
   /****************************************************************************/
 
   /**
-   * Get webform logger.
+   * Get webform or webform_submission logger.
+   *
+   * @param string $channel
+   *   The logger channel. Defaults to 'webform'.
    *
    * @return \Drupal\Core\Logger\LoggerChannelInterface
    *   Webform logger
    */
-  protected function getLogger() {
-    return $this->loggerFactory->get('webform');
+  protected function getLogger($channel = 'webform') {
+    return $this->loggerFactory->get($channel);
   }
 
   /**
@@ -612,6 +761,18 @@ protected function getLogger() {
    *   The message to be logged.
    * @param array $data
    *   The data to be saved with log record.
+   *
+   * @deprecated Instead call the 'webform_submission' logger channel directly.
+   *
+   *  $message = 'Some message with an %argument.'
+   *  $context = [
+   *    '%argument' => 'Some value'
+   *    'link' => $webform_submission->toLink($this->t('Edit'), 'edit-form')->toString(),
+   *    'webform_submission' => $webform_submission,
+   *    'handler_id' => NULL,
+   *    'data' => [],
+   *  ];
+   *  \Drupal::logger('webform_submission')->notice($message, $context);
    */
   protected function log(WebformSubmissionInterface $webform_submission, $operation, $message = '', array $data = []) {
     if ($webform_submission->getWebform()->hasSubmissionLog()) {
@@ -624,4 +785,38 @@ protected function log(WebformSubmissionInterface $webform_submission, $operatio
     }
   }
 
+  /****************************************************************************/
+  // TEMP: Messenger methods to be remove once Drupal 8.6.x+ is supported version.
+  /****************************************************************************/
+
+  /**
+   * The messenger.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
+   * Sets the messenger.
+   *
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger.
+   */
+  public function setMessenger(MessengerInterface $messenger) {
+    $this->messenger = $messenger;
+  }
+
+  /**
+   * Gets the messenger.
+   *
+   * @return \Drupal\Core\Messenger\MessengerInterface
+   *   The messenger.
+   */
+  public function messenger() {
+    if (!isset($this->messenger)) {
+      $this->messenger = \Drupal::messenger();
+    }
+    return $this->messenger;
+  }
+
 }
diff --git a/web/modules/webform/src/Plugin/WebformHandlerInterface.php b/web/modules/webform/src/Plugin/WebformHandlerInterface.php
index cb37038b67a74d1606b83d1353d3a79a8a5d96de..bfe6885419c85fa2f78eb0e9a529cc5580d37a73 100644
--- a/web/modules/webform/src/Plugin/WebformHandlerInterface.php
+++ b/web/modules/webform/src/Plugin/WebformHandlerInterface.php
@@ -91,6 +91,14 @@ public function cardinality();
    */
   public function supportsConditions();
 
+  /**
+   * Determine if webform handler supports tokens.
+   *
+   * @return bool
+   *   TRUE if the webform handler supports tokens.
+   */
+  public function supportsTokens();
+
   /**
    * Returns the unique ID representing the webform handler.
    *
@@ -181,6 +189,20 @@ public function getConditions();
    */
   public function setConditions(array $conditions);
 
+  /**
+   * Enables the webform handler.
+   *
+   * @return $this
+   */
+  public function enable();
+
+  /**
+   * Disables the webform handler.
+   *
+   * @return $this
+   */
+  public function disable();
+
   /**
    * Checks if the handler is excluded via webform.settings.
    *
@@ -224,10 +246,10 @@ public function isSubmissionOptional();
   public function isSubmissionRequired();
 
   /**
-   * Initialize webform handler.
+   * Set the webform that this is handler is attached to.
    *
    * @param \Drupal\webform\WebformInterface $webform
-   *   A webform object.
+   *   A webform.
    *
    * @return $this
    *   This webform handler.
@@ -242,13 +264,32 @@ public function setWebform(WebformInterface $webform);
    */
   public function getWebform();
 
+  /**
+   * Set the webform submission that this handler is handling.
+   *
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   *
+   * @return $this
+   *   This webform handler.
+   */
+  public function setWebformSubmission(WebformSubmissionInterface $webform_submission = NULL);
+
+  /**
+   * Get the webform submission that this handler is handling.
+   *
+   * @return \Drupal\webform\WebformSubmissionInterface
+   *   A webform submission.
+   */
+  public function getWebformSubmission();
+
   /**
    * Check handler conditions against a webform submission.
    *
    * Note: Conditions are only applied to callbacks that require a
    * webform submissions.
    *
-   * Conditions are ignored by...
+   * Conditions are ignored by…
    * - \Drupal\webform\Plugin\WebformHandlerInterface::alterElements
    * - \Drupal\webform\Plugin\WebformHandlerInterface::preCreate
    *
diff --git a/web/modules/webform/src/Plugin/WebformSourceEntity/QueryStringWebformSourceEntity.php b/web/modules/webform/src/Plugin/WebformSourceEntity/QueryStringWebformSourceEntity.php
index 050584058baba01065314995f6d399f9090c2975..633e471e606efb514e7fd7fb41aa60534304ff8a 100644
--- a/web/modules/webform/src/Plugin/WebformSourceEntity/QueryStringWebformSourceEntity.php
+++ b/web/modules/webform/src/Plugin/WebformSourceEntity/QueryStringWebformSourceEntity.php
@@ -59,22 +59,6 @@ class QueryStringWebformSourceEntity extends PluginBase implements WebformSource
    */
   protected $webformEntityReferenceManager;
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    return new static(
-      $configuration,
-      $plugin_id,
-      $plugin_definition,
-      $container->get('entity_type.manager'),
-      $container->get('current_route_match'),
-      $container->get('request_stack'),
-      $container->get('language_manager'),
-      $container->get('webform.entity_reference_manager')
-    );
-  }
-
   /**
    * QueryStringWebformSourceEntity constructor.
    *
@@ -105,6 +89,22 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
     $this->webformEntityReferenceManager = $webform_entity_reference_manager;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+      $container->get('current_route_match'),
+      $container->get('request_stack'),
+      $container->get('language_manager'),
+      $container->get('webform.entity_reference_manager')
+    );
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -134,7 +134,8 @@ public function getSourceEntity(array $ignored_types) {
       return NULL;
     }
 
-    if (is_subclass_of($source_entity, TranslatableInterface::class) && $source_entity->hasTranslation($this->languageManager->getCurrentLanguage()->getId())) {
+    // Get translated source entity.
+    if ($source_entity instanceof TranslatableInterface && $source_entity->hasTranslation($this->languageManager->getCurrentLanguage()->getId())) {
       $source_entity = $source_entity->getTranslation($this->languageManager->getCurrentLanguage()->getId());
     }
 
@@ -146,16 +147,18 @@ public function getSourceEntity(array $ignored_types) {
     // Check that the webform is referenced by the source entity.
     if (!$webform->getSetting('form_prepopulate_source_entity')) {
       // Get source entity's webform field.
-      $webform_field_name = $this->webformEntityReferenceManager->getFieldName($source_entity);
-      if (!$webform_field_name) {
-        return NULL;
+      $webform_field_names = $this->webformEntityReferenceManager->getFieldNames($source_entity);
+      foreach ($webform_field_names as $webform_field_name) {
+        // Check that source entity's reference webform is the
+        // current webform.
+        foreach ($source_entity->$webform_field_name as $item) {
+          if ($item->target_id === $webform->id()) {
+            return $source_entity;
+          }
+        }
       }
 
-      // Check that source entity's reference webform is the current YAML
-      // webform.
-      if ($source_entity->$webform_field_name->target_id != $webform->id()) {
-        return NULL;
-      }
+      return NULL;
     }
 
     return $source_entity;
diff --git a/web/modules/webform/src/Plugin/WebformSourceEntity/RouteParametersWebformSourceEntity.php b/web/modules/webform/src/Plugin/WebformSourceEntity/RouteParametersWebformSourceEntity.php
index 18dcdcf8efde0af2bca9bfb99e3eb94caf45ae89..fba4d6264289c95a902c5d3dbf97c0b04b93b78a 100644
--- a/web/modules/webform/src/Plugin/WebformSourceEntity/RouteParametersWebformSourceEntity.php
+++ b/web/modules/webform/src/Plugin/WebformSourceEntity/RouteParametersWebformSourceEntity.php
@@ -27,18 +27,6 @@ class RouteParametersWebformSourceEntity extends PluginBase implements WebformSo
    */
   protected $routeMatch;
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    return new static(
-      $configuration,
-      $plugin_id,
-      $plugin_definition,
-      $container->get('current_route_match')
-    );
-  }
-
   /**
    * RouteParametersWebformSourceEntity constructor.
    *
@@ -57,10 +45,28 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
     $this->routeMatch = $route_match;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('current_route_match')
+    );
+  }
+
   /**
    * {@inheritdoc}
    */
   public function getSourceEntity(array $ignored_types) {
+    // Use current account when viewing a user's submissions.
+    // @see \Drupal\webform\WebformSubmissionListBuilder
+    if ($this->routeMatch->getRouteName() === 'entity.webform_submission.user') {
+      return NULL;
+    }
+
     // Get the most specific source entity available in the current route's
     // parameters.
     $parameters = $this->routeMatch->getParameters()->all();
diff --git a/web/modules/webform/src/Plugin/WebformSourceEntityInterface.php b/web/modules/webform/src/Plugin/WebformSourceEntityInterface.php
index 99a8860982bc3c3fef446b11359822d752efa932..536d8b6d1f3c658157a2554a54b29b72a1967ae1 100644
--- a/web/modules/webform/src/Plugin/WebformSourceEntityInterface.php
+++ b/web/modules/webform/src/Plugin/WebformSourceEntityInterface.php
@@ -13,10 +13,10 @@ interface WebformSourceEntityInterface extends PluginInspectionInterface {
    * Detect and return a source entity from current context.
    *
    * @param string[] $ignored_types
-   *   Entity types that may not be source entity.
+   *   Entity types that may not be used as a source entity.
    *
    * @return \Drupal\Core\Entity\EntityInterface|null
-   *   Source entity or NULL should no source entity be found.
+   *   Source entity or NULL when no source entity is found.
    */
   public function getSourceEntity(array $ignored_types);
 
diff --git a/web/modules/webform/src/Plugin/WebformSourceEntityManager.php b/web/modules/webform/src/Plugin/WebformSourceEntityManager.php
index 4f7046b54df8f4fdb675b32e052a9ded4d742c85..0dadf72aec26b5d8118fe0f42c0a172cd96c20ee 100644
--- a/web/modules/webform/src/Plugin/WebformSourceEntityManager.php
+++ b/web/modules/webform/src/Plugin/WebformSourceEntityManager.php
@@ -4,6 +4,7 @@
 
 use Drupal\Component\Utility\SortArray;
 use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\DefaultPluginManager;
 
@@ -35,6 +36,47 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac
     $this->setCacheBackend($cache_backend, 'webform_source_entity_info_plugins');
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function alterDefinitions(&$definitions) {
+    // Unset elements that are missing target element or dependencies.
+    foreach ($definitions as $element_key => $element_definition) {
+      // Check element's (module) dependencies exist.
+      foreach ($element_definition['dependencies'] as $dependency) {
+        if (!$this->moduleHandler->moduleExists($dependency)) {
+          unset($definitions[$element_key]);
+          continue;
+        }
+      }
+    }
+
+    // Additionally sort by weight so we always have them sorted in proper
+    // order.
+    uasort($definitions, [SortArray::class, 'sortByWeightElement']);
+
+    parent::alterDefinitions($definitions);
+  }
+
+  /**
+   * Get the main source entity. Applies to only paragraphs.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $source_entity
+   *   A source entity.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   The main source entity.
+   *
+   * @see \Drupal\webform\Plugin\Field\FieldFormatter\WebformEntityReferenceEntityFormatter::viewElements
+   * @see \Drupal\webform\Plugin\WebformSourceEntity\QueryStringWebformSourceEntity::getSourceEntity
+   */
+  public static function getMainSourceEntity(EntityInterface $source_entity) {
+    while ($source_entity && $source_entity->getEntityTypeId() === 'paragraph') {
+      $source_entity = $source_entity->getParentEntity();
+    }
+    return $source_entity;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -55,15 +97,4 @@ public function getSourceEntity($ignored_types = []) {
     return NULL;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function alterDefinitions(&$definitions) {
-    parent::alterDefinitions($definitions);
-
-    // Additionally sort by weight so we always have them sorted in proper
-    // order.
-    uasort($definitions, [SortArray::class, 'sortByWeightElement']);
-  }
-
 }
diff --git a/web/modules/webform/src/Plugin/WebformSourceEntityManagerInterface.php b/web/modules/webform/src/Plugin/WebformSourceEntityManagerInterface.php
index 5487804baae0f06c7b97db8c57426044f1d3422b..8e88a7da37486cbc8ea149e3fbdf08d4fb6b8f4a 100644
--- a/web/modules/webform/src/Plugin/WebformSourceEntityManagerInterface.php
+++ b/web/modules/webform/src/Plugin/WebformSourceEntityManagerInterface.php
@@ -13,10 +13,10 @@ interface WebformSourceEntityManagerInterface extends PluginManagerInterface {
    * Detect and return a source entity from current context.
    *
    * @param string|string[] $ignored_types
-   *   Entity types that may not be source entity.
+   *   Entity types that may not be used as a source entity.
    *
    * @return \Drupal\Core\Entity\EntityInterface|null
-   *   Source entity or NULL should no source entity be found
+   *   Source entity or NULL when no source entity is found.
    */
   public function getSourceEntity($ignored_types = []);
 
diff --git a/web/modules/webform/src/Routing/WebformRouteSubscriber.php b/web/modules/webform/src/Routing/WebformRouteSubscriber.php
index 0171020fa0728d21bd96975f98038f6605f429f6..1259c61004a6da95080f6a29215cbf96677d4b51 100644
--- a/web/modules/webform/src/Routing/WebformRouteSubscriber.php
+++ b/web/modules/webform/src/Routing/WebformRouteSubscriber.php
@@ -14,7 +14,7 @@ class WebformRouteSubscriber extends RouteSubscriberBase {
    * {@inheritdoc}
    */
   protected function alterRoutes(RouteCollection $collection) {
-    // Reove 'Contribute' route if explicitly disabled or the Contribute module
+    // Remove 'Contribute' route if explicitly disabled or the Contribute module
     // is installed.
     if (\Drupal::config('webform.settings')->get('ui.contribute_disabled') || \Drupal::moduleHandler()->moduleExists('contribute')) {
       $collection->remove('webform.contribute');
diff --git a/web/modules/webform/src/Tests/Access/WebformAccessEntityJsonApiTest.php b/web/modules/webform/src/Tests/Access/WebformAccessEntityJsonApiTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..dc8790e685c82b1f2b07d809aa7aeff6115e9675
--- /dev/null
+++ b/web/modules/webform/src/Tests/Access/WebformAccessEntityJsonApiTest.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Drupal\webform\Tests\Access;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
+
+/**
+ * Tests for webform entity JSON API access.
+ *
+ * @group Webform
+ */
+class WebformAccessEntityJsonApiTest extends WebformTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'jsonapi'];
+
+  /**
+   * Tests webform entity REST acces.
+   */
+  public function testRestAccess() {
+    $webform = Webform::load('contact');
+    $uuid = $webform->uuid();
+
+    $account = $this->drupalCreateUser();
+
+    $configuration_account = $this->drupalCreateUser([
+      'access any webform configuration',
+    ]);
+
+    /**************************************************************************/
+
+    // Check anonymous access denied to webform.
+    $this->drupalGet("jsonapi/webform/webform/$uuid");
+    $this->assertRaw('"title":"Forbidden","status":"403","detail":"The current user is not allowed to GET the selected resource. Access to webform configuration is required."');
+
+    // Login authenticated user.
+    $this->drupalLogin($account);
+
+    // Check authenticated access allowed to webform.
+    $this->drupalGet('/webform/contact');
+    $this->assertFieldByName('subject');
+
+    // Check authenticated access denied to webform via _format=hal_json.
+    $this->drupalGet("jsonapi/webform/webform/$uuid");
+    $this->assertRaw('"title":"Forbidden","status":"403","detail":"The current user is not allowed to GET the selected resource. Access to webform configuration is required."');
+
+    // Login rest (permission) user.
+    $this->drupalLogin($configuration_account);
+
+    // Check rest access allowed to webform.
+    $this->drupalGet("jsonapi/webform/webform/$uuid");
+    $this->assertNoRaw('"title":"Forbidden","status":"403","detail":"The current user is not allowed to GET the selected resource. Access to webform configuration is required."');
+    $this->assertRaw('"title":"Contact",');
+
+    // Allow anonymous role to access webform configuration.
+    $access_rules = $webform->getAccessRules();
+    $access_rules['configuration']['roles'] = ['anonymous', 'authenticated'];
+    $webform->setAccessRules($access_rules);
+    $webform->save();
+
+    // Login out and switch to anonymous user.
+    $this->drupalLogout();
+
+    // Check anonymous access allowed to webform.
+    $this->drupalGet("jsonapi/webform/webform/$uuid");
+    $this->assertNoRaw('"title":"Forbidden","status":"403","detail":"The current user is not allowed to GET the selected resource. Access to webform configuration is required."');
+
+    // Login authenticated user.
+    $this->drupalLogin($account);
+
+    // Check authenticated access allowed to webform.
+    $this->drupalGet("jsonapi/webform/webform/$uuid");
+    $this->assertNoRaw('"title":"Forbidden","status":"403","detail":"The current user is not allowed to GET the selected resource. Access to webform configuration is required."');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Access/WebformAccessEntityPermissionsTest.php b/web/modules/webform/src/Tests/Access/WebformAccessEntityPermissionsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..f322fee92cf6b59204fd44c2fa055a1f12985c5b
--- /dev/null
+++ b/web/modules/webform/src/Tests/Access/WebformAccessEntityPermissionsTest.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Drupal\webform\Tests\Access;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
+
+/**
+ * Tests for webform entity permissions.
+ *
+ * @group Webform
+ */
+class WebformAccessEntityPermissionsTest extends WebformTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['node', 'webform', 'webform_test_submissions'];
+
+  /**
+   * Tests webform entity access controls.
+   */
+  public function testAccessControlHandler() {
+    $own_account = $this->drupalCreateUser([
+      'access webform overview',
+      'create webform',
+      'edit own webform',
+      'delete own webform',
+    ]);
+    $any_account = $this->drupalCreateUser([
+      'access webform overview',
+      'create webform',
+      'edit any webform',
+      'delete any webform',
+    ]);
+
+    /**************************************************************************/
+
+    // Login as user who can access own webform.
+    $this->drupalLogin($own_account);
+
+    // Check create own webform.
+    $this->drupalPostForm('admin/structure/webform/add', ['id' => 'test_own', 'title' => 'test_own'], t('Save'));
+
+    // Check webform submission overview contains own webform.
+    $this->drupalGet('/admin/structure/webform');
+    $this->assertRaw('test_own');
+
+    // Add test element to own webform.
+    $this->drupalPostForm('/admin/structure/webform/manage/test_own', ['elements' => "test:\n  '#markup': 'test'"], t('Save'));
+
+    // Check duplicate own webform.
+    $this->drupalGet('/admin/structure/webform/manage/test_own/duplicate');
+    $this->assertResponse(200);
+
+    // Check delete own webform.
+    $this->drupalGet('/admin/structure/webform/manage/test_own/delete');
+    $this->assertResponse(200);
+
+    // Check access own webform submissions.
+    $this->drupalGet('/admin/structure/webform/manage/test_own/results/submissions');
+    $this->assertResponse(200);
+
+    // Login as user who can access any webform.
+    $this->drupalLogin($any_account);
+
+    // Check duplicate any webform.
+    $this->drupalGet('/admin/structure/webform/manage/test_own/duplicate');
+    $this->assertResponse(200);
+
+    // Check delete any webform.
+    $this->drupalGet('/admin/structure/webform/manage/test_own/delete');
+    $this->assertResponse(200);
+
+    // Check access any webform submissions.
+    $this->drupalGet('/admin/structure/webform/manage/test_own/results/submissions');
+    $this->assertResponse(200);
+
+    // Change the owner of the webform to 'any' user.
+    $own_webform = Webform::load('test_own');
+    $own_webform->setOwner($any_account)->save();
+
+    // Login as user who can access own webform.
+    $this->drupalLogin($own_account);
+
+    // Check webform submission overview does not contains any webform.
+    $this->drupalGet('/admin/structure/webform');
+    $this->assertNoRaw('test_own');
+
+    // Check duplicate denied any webform.
+    $this->drupalGet('/admin/structure/webform/manage/test_own/duplicate');
+    $this->assertResponse(403);
+
+    // Check delete denied any webform.
+    $this->drupalGet('/admin/structure/webform/manage/test_own/delete');
+    $this->assertResponse(403);
+
+    // Check access denied any webform submissions.
+    $this->drupalGet('/admin/structure/webform/manage/test_own/results/submissions');
+    $this->assertResponse(403);
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Access/WebformAccessEntityRestTest.php b/web/modules/webform/src/Tests/Access/WebformAccessEntityRestTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..dc7104d4ca5ea094f5bcd797763a035e7c367fff
--- /dev/null
+++ b/web/modules/webform/src/Tests/Access/WebformAccessEntityRestTest.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Drupal\webform\Tests\Access;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
+
+/**
+ * Tests for webform entity REST access.
+ *
+ * @group Webform
+ */
+class WebformAccessEntityRestTest extends WebformTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'webform_test_rest'];
+
+  /**
+   * Tests webform entity REST acces.
+   */
+  public function testRestAccess() {
+    $webform = Webform::load('contact');
+
+    $account = $this->drupalCreateUser();
+
+    $configuration_account = $this->drupalCreateUser([
+      'access any webform configuration',
+    ]);
+
+    /**************************************************************************/
+
+    // Check anonymous access denied to webform via _format=hal_json.
+    $this->drupalGet('/webform/contact', ['query' => ['_format' => 'hal_json']]);
+    $this->assertRaw('{"message":"Access to webform configuration is required."}');
+
+    // Login authenticated user.
+    $this->drupalLogin($account);
+
+    // Check authenticated access allowed to webform via _format=html.
+    $this->drupalGet('/webform/contact');
+    $this->assertFieldByName('subject');
+
+    // Check authenticated access denied to webform via _format=hal_json.
+    $this->drupalGet('/webform/contact', ['query' => ['_format' => 'hal_json']]);
+    $this->assertRaw('{"message":"Access to webform configuration is required."}');
+
+    // Login rest (permission) user.
+    $this->drupalLogin($configuration_account);
+
+    // Check rest access allowed to webform via _format=hal_json.
+    $this->drupalGet('/webform/contact', ['query' => ['_format' => 'hal_json']]);
+    $this->assertNoRaw('{"message":"Access to webform configuration is required."}');
+    $this->assertRaw('"id":"contact","title":"Contact"');
+
+    // Allow anonymous role to access webform configuration.
+    $access_rules = $webform->getAccessRules();
+    $access_rules['configuration']['roles'] = ['anonymous', 'authenticated'];
+    $webform->setAccessRules($access_rules);
+    $webform->save();
+
+    // Login out and switch to anonymous user.
+    $this->drupalLogout();
+
+    // Check anonymous access allowed to webform via _format=hal_json.
+    $this->drupalGet('/webform/contact', ['query' => ['_format' => 'hal_json']]);
+    $this->assertNoRaw('{"message":"Access to webform configuration is required."}');
+
+    // Login authenticated user.
+    $this->drupalLogin($account);
+
+    // Check authenticated access allowed to webform via _format=hal_json.
+    $this->drupalGet('/webform/contact', ['query' => ['_format' => 'hal_json']]);
+    $this->assertNoRaw('{"message":"Access to webform configuration is required."}');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/WebformEntityAccessTest.php b/web/modules/webform/src/Tests/Access/WebformAccessEntityRulesTest.php
similarity index 66%
rename from web/modules/webform/src/Tests/WebformEntityAccessTest.php
rename to web/modules/webform/src/Tests/Access/WebformAccessEntityRulesTest.php
index c74bb1bbc277ac6952a1a0c44d7c0dc9291cc478..77cbbba5537c3b53e5113b8e7eac75083a39dde3 100644
--- a/web/modules/webform/src/Tests/WebformEntityAccessTest.php
+++ b/web/modules/webform/src/Tests/Access/WebformAccessEntityRulesTest.php
@@ -1,16 +1,17 @@
 <?php
 
-namespace Drupal\webform\Tests;
+namespace Drupal\webform\Tests\Access;
 
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
 
 /**
- * Tests for webform access controls.
+ * Tests for webform entity access rules.
  *
  * @group Webform
  */
-class WebformEntityAccessTest extends WebformTestBase {
+class WebformAccessEntityRulesTest extends WebformTestBase {
 
   /**
    * Modules to enable.
@@ -27,142 +28,106 @@ class WebformEntityAccessTest extends WebformTestBase {
   protected static $testWebforms = ['test_submissions'];
 
   /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
-  /**
-   * Tests webform access rules.
-   */
-  public function testAccessControlHandler() {
-    // Login as user who can access own webform.
-    $this->drupalLogin($this->ownWebformUser);
-
-    // Check create own webform.
-    $this->drupalPostForm('admin/structure/webform/add', ['id' => 'test_own', 'title' => 'test_own'], t('Save'));
-
-    // Add test element to own webform.
-    $this->drupalPostForm('/admin/structure/webform/manage/test_own', ['elements' => "test:\n  '#markup': 'test'"], t('Save'));
-
-    // Check duplicate own webform.
-    $this->drupalGet('admin/structure/webform/manage/test_own/duplicate');
-    $this->assertResponse(200);
-
-    // Check delete own webform.
-    $this->drupalGet('admin/structure/webform/manage/test_own/delete');
-    $this->assertResponse(200);
-
-    // Check access own webform submissions.
-    $this->drupalGet('admin/structure/webform/manage/test_own/results/submissions');
-    $this->assertResponse(200);
-
-    // Login as user who can access any webform.
-    $this->drupalLogin($this->anyWebformUser);
-
-    // Check duplicate any webform.
-    $this->drupalGet('admin/structure/webform/manage/test_own/duplicate');
-    $this->assertResponse(200);
-
-    // Check delete any webform.
-    $this->drupalGet('admin/structure/webform/manage/test_own/delete');
-    $this->assertResponse(200);
-
-    // Check access any webform submissions.
-    $this->drupalGet('admin/structure/webform/manage/test_own/results/submissions');
-    $this->assertResponse(200);
-
-    // Change the owner of the webform to 'any' user.
-    $own_webform = Webform::load('test_own');
-    $own_webform->setOwner($this->anyWebformUser)->save();
-
-    // Login as user who can access own webform.
-    $this->drupalLogin($this->ownWebformUser);
-
-    // Check duplicate denied any webform.
-    $this->drupalGet('admin/structure/webform/manage/test_own/duplicate');
-    $this->assertResponse(403);
-
-    // Check delete denied any webform.
-    $this->drupalGet('admin/structure/webform/manage/test_own/delete');
-    $this->assertResponse(403);
-
-    // Check access denied any webform submissions.
-    $this->drupalGet('admin/structure/webform/manage/test_own/results/submissions');
-    $this->assertResponse(403);
-  }
-
-  /**
-   * Tests webform access rules.
+   * Tests webform entity access rules.
    */
   public function testAccessRules() {
     global $base_path;
 
+    /** @var \Drupal\webform\WebformAccessRulesManagerInterface $access_rules_manager */
+    $access_rules_manager = \Drupal::service('webform.access_rules_manager');
+    $default_access_rules = $access_rules_manager->getDefaultAccessRules();
+
     /** @var \Drupal\webform\WebformInterface $webform */
     $webform = Webform::load('test_submissions');
     /** @var \Drupal\webform\WebformSubmissionInterface[] $submissions */
     $submissions = array_values(\Drupal::entityTypeManager()->getStorage('webform_submission')->loadByProperties(['webform_id' => 'test_submissions']));
 
-    $account = $this->drupalCreateUser(['access content']);
+    $account = $this->drupalCreateUser(['access content', 'edit webform source']);
 
     $webform_id = $webform->id();
     $sid = $submissions[0]->id();
     $uid = $account->id();
-    $rid = $account->getRoles()[1];
+    $rid = $account->getRoles(TRUE)[0];
+
+    /**************************************************************************/
+    // Test.
+    /**************************************************************************/
+
+    $this->drupalLogin($account);
 
-    // Check 'test' access rule.
+    // Check that user cannot access test form.
     $this->drupalGet("webform/$webform_id/test");
     $this->assertResponse(403, 'Webform setting access denied for test rule.');
+
+    // Assign user to 'test' access rule.
     $access_rules = [
       'test' => [
         'roles' => [],
         'users' => [$uid],
         'permissions' => [],
       ],
-    ] + Webform::getDefaultAccessRules();
+    ] + $default_access_rules;
     $webform->setAccessRules($access_rules)->save();
-    $this->drupalLogin($account);
+
+    // Check that user can access test form.
     $this->drupalGet("webform/$webform_id/test");
     $this->assertResponse(200, 'Webform setting access for test rule.');
-    $this->drupalLogout($account);
 
-    // Check 'administer' access rule.
+    /**************************************************************************/
+    // Administer.
+    /**************************************************************************/
+
+    // Check that user cannot access form settings.
+    $this->drupalGet("admin/structure/webform/manage/$webform_id/settings");
+    $this->assertResponse(403, 'Webform setting access denied for administer rule.');
+    $this->drupalGet("admin/structure/webform/manage/$webform_id/results/submissions");
+    $this->assertResponse(403, 'Webform submissions access denied for administer rule.');
+
+    // Assign user to 'administer' access rule.
     $access_rules = [
       'administer' => [
         'roles' => [],
         'users' => [$uid],
         'permissions' => [],
       ],
-    ] + Webform::getDefaultAccessRules();
+    ] + $default_access_rules;
     $webform->setAccessRules($access_rules)->save();
-    $this->drupalLogin($account);
+
+    // Check that user cannot access settings.
     $this->drupalGet("admin/structure/webform/manage/$webform_id/settings");
-    $this->assertResponse(200, 'Webform setting access for administer rule.');
+    $this->assertResponse(200, 'Webform setting access allowed for administer rule.');
     $this->drupalGet("admin/structure/webform/manage/$webform_id/results/submissions");
-    $this->assertResponse(200, 'Webform submissions access for administer rule.');
-    $this->drupalLogout($account);
+    $this->assertResponse(200, 'Webform submissions access allowed for administer rule.');
+
+    /**************************************************************************/
+    // Create.
+    /**************************************************************************/
+
+    $this->drupalLogout();
 
     // Check create authenticated/anonymous access.
-    $webform->setAccessRules(Webform::getDefaultAccessRules())->save();
-    $this->drupalGet('webform/' . $webform->id());
-    $this->assertResponse(200, 'Webform create submission access for anonymous/authenticated user.');
+    $webform->setAccessRules($default_access_rules)->save();
+    $this->drupalGet('/webform/' . $webform->id());
+    $this->assertResponse(200, 'Webform create submission access allowed for anonymous/authenticated user.');
 
+    // Revoke create from anonymous and authenticated roles.
     $access_rules = [
       'create' => [
         'roles' => [],
         'users' => [],
         'permissions' => [],
       ],
-    ] + Webform::getDefaultAccessRules();
+    ] + $default_access_rules;
     $webform->setAccessRules($access_rules)->save();
 
-    // Check no access.
-    $this->drupalGet('webform/' . $webform->id());
+    // Check create access denied.
+    $this->drupalGet('/webform/' . $webform->id());
     $this->assertResponse(403, 'Webform returns access denied');
+
+    /**************************************************************************/
+    // Any.
+    /**************************************************************************/
+
     $any_tests = [
       'webform/{webform}' => 'create',
       'admin/structure/webform/manage/{webform}/results/submissions' => 'view_any',
@@ -184,6 +149,7 @@ public function testAccessRules() {
       $this->assertResponse(403, 'Webform returns access denied');
     }
 
+    // Login.
     $this->drupalLogin($account);
 
     // Check that all the test paths are access denied for authenticated.
@@ -195,7 +161,7 @@ public function testAccessRules() {
       $this->assertResponse(403, 'Webform returns access denied');
     }
 
-    // Check access rules by role, user id, and permission.
+    // Check any access rules by role, user id, and permission.
     foreach ($any_tests as $path => $permission) {
       $path = str_replace('{webform}', $webform_id, $path);
       $path = str_replace('{webform_submission}', $sid, $path);
@@ -207,7 +173,7 @@ public function testAccessRules() {
           'users' => [],
           'permissions' => [],
         ],
-      ] + Webform::getDefaultAccessRules();
+      ] + $default_access_rules;
       $webform->setAccessRules($access_rules)->save();
       $this->drupalGet($path);
       $this->assertResponse(200, 'Webform allows access via role access rules');
@@ -219,24 +185,28 @@ public function testAccessRules() {
           'users' => [$uid],
           'permissions' => [],
         ],
-      ] + Webform::getDefaultAccessRules();
+      ] + $default_access_rules;
       $webform->setAccessRules($access_rules)->save();
       $this->drupalGet($path);
       $this->assertResponse(200, 'Webform allows access via user access rules');
 
       // Check access rule via 'access content'.
       $access_rules = [
-          $permission => [
-            'roles' => [],
-            'users' => [],
-            'permissions' => ['access content'],
-          ],
-        ] + Webform::getDefaultAccessRules();
+        $permission => [
+          'roles' => [],
+          'users' => [],
+          'permissions' => ['access content'],
+        ],
+      ] + $default_access_rules;
       $webform->setAccessRules($access_rules)->save();
       $this->drupalGet($path);
       $this->assertResponse(200, "Webform allows access via permission access rules");
     }
 
+    /**************************************************************************/
+    // Own.
+    /**************************************************************************/
+
     // Check own / user specific access rules.
     $access_rules = [
       'view_own' => [
@@ -254,7 +224,7 @@ public function testAccessRules() {
         'users' => [],
         'permissions' => [],
       ],
-    ] + Webform::getDefaultAccessRules();
+    ] + $default_access_rules;
     $webform->setAccessRules($access_rules)->save();
 
     // Must delete all existing anonymous submission to prevent them from
@@ -267,30 +237,24 @@ public function testAccessRules() {
     $this->drupalLogin($account);
 
     // Check no view previous submission message.
-    $this->drupalGet('webform/' . $webform->id());
+    $this->drupalGet('/webform/' . $webform->id());
     $this->assertNoRaw('You have already submitted this webform.');
     $this->assertNoRaw('View your previous submission');
 
     $sid = $this->postSubmission($webform);
 
     // Check view previous submission message.
-    $this->drupalGet('webform/' . $webform->id());
+    $this->drupalGet('/webform/' . $webform->id());
     $this->assertRaw('You have already submitted this webform.');
     $this->assertRaw("<a href=\"{$base_path}webform/{$webform_id}/submissions/{$sid}\">View your previous submission</a>.");
 
     $sid = $this->postSubmission($webform);
 
     // Check view previous submissions message.
-    $this->drupalGet('webform/' . $webform->id());
+    $this->drupalGet('/webform/' . $webform->id());
     $this->assertRaw('You have already submitted this webform.');
     $this->assertRaw("<a href=\"{$base_path}webform/{$webform_id}/submissions\">View your previous submissions</a>");
 
-    // Check disabled previous submissions messages.
-    $webform->setSetting('form_previous_submissions', FALSE);
-    $webform->save();
-    $this->drupalGet('webform/' . $webform->id());
-    $this->assertNoRaw('You have already submitted this webform.');
-
     // Check the new submission's view, update, and delete access for the user.
     $test_own = [
       'admin/structure/webform/manage/{webform}/results/submissions' => 403,
@@ -301,6 +265,10 @@ public function testAccessRules() {
       'admin/structure/webform/manage/{webform}/submission/{webform_submission}/yaml' => 403,
       'admin/structure/webform/manage/{webform}/submission/{webform_submission}/edit' => 200,
       'admin/structure/webform/manage/{webform}/submission/{webform_submission}/delete' => 200,
+      'webform/{webform}/submissions/{webform_submission}' => 200,
+      'webform/{webform}/submissions/{webform_submission}/edit' => 200,
+      'webform/{webform}/submissions/{webform_submission}/duplicate' => 403,
+      'webform/{webform}/submissions/{webform_submission}/delete' => 200,
     ];
     foreach ($test_own as $path => $status_code) {
       $path = str_replace('{webform}', $webform_id, $path);
@@ -309,6 +277,20 @@ public function testAccessRules() {
       $this->drupalGet($path);
       $this->assertResponse($status_code, new FormattableMarkup('Webform @status_code access via own access rules.', ['@status_code' => ($status_code == 403 ? 'denies' : 'allows')]));
     }
+
+    // Enable submission user duplicate.
+    $webform->setSetting('submission_user_duplicate', TRUE);
+    $webform->save();
+
+    // Check enable user submission duplicate.
+    $this->drupalGet("webform/$webform_id/submissions/$sid/duplicate");
+    $this->assertResponse(200);
+
+    // Check disabled previous submissions messages.
+    $webform->setSetting('form_previous_submissions', FALSE);
+    $webform->save();
+    $this->drupalGet('/webform/' . $webform->id());
+    $this->assertNoRaw('You have already submitted this webform.');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Access/WebformAccessSubmissionPermissionsTest.php b/web/modules/webform/src/Tests/Access/WebformAccessSubmissionPermissionsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..3020bc1c6a3df17516f4c1ab5ac10f089b3b591d
--- /dev/null
+++ b/web/modules/webform/src/Tests/Access/WebformAccessSubmissionPermissionsTest.php
@@ -0,0 +1,239 @@
+<?php
+
+namespace Drupal\webform\Tests\Access;
+
+use Drupal\user\Entity\Role;
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
+
+/**
+ * Tests for webform submission permissions.
+ *
+ * @group Webform
+ */
+class WebformAccessSubmissionPermissionsTest extends WebformTestBase {
+
+  /**
+   * Test webform submission access permissions.
+   */
+  public function testPermissions() {
+    global $base_path;
+
+    $admin_webform_account = $this->drupalCreateUser([
+      'administer webform',
+      'create webform',
+    ]);
+
+    $admin_submission_account = $this->drupalCreateUser([
+      'administer webform submission',
+    ]);
+
+    $own_webform_account = $this->drupalCreateUser([
+      'edit own webform',
+    ]);
+
+    $any_submission_account = $this->drupalCreateUser([
+      'view any webform submission',
+      'edit any webform submission',
+      'delete any webform submission',
+    ]);
+
+    $own_submission_account = $this->drupalCreateUser([
+      'view own webform submission',
+      'edit own webform submission',
+      'delete own webform submission',
+      'access webform submission user',
+    ]);
+
+    $webform_id = 'contact';
+    $webform = Webform::load('contact');
+
+    /**************************************************************************/
+    // Create submission permissions (anonymous).
+    /**************************************************************************/
+
+    $edit = ['subject' => '{subject}', 'message' => '{message}'];
+    $sid_1 = $this->postSubmission($webform, $edit);
+
+    // Check cannot view own submissions.
+    $uid = $own_submission_account->id();
+    $this->drupalGet("user/$uid/submissions");
+    $this->assertResponse(403);
+
+    // Check cannot view own previous submission message.
+    $this->drupalGet('/webform/' . $webform->id());
+    $this->assertNoRaw('You have already submitted this webform.');
+
+    // Check cannot 'view own submission' permission.
+    $this->drupalGet("webform/{$webform_id}/submissions/{$sid_1}");
+    $this->assertResponse(403);
+
+    /**************************************************************************/
+    // Own submission permissions (authenticated).
+    /**************************************************************************/
+
+    $this->drupalLogin($own_submission_account);
+
+    $edit = ['subject' => '{subject}', 'message' => '{message}'];
+    $sid_2 = $this->postSubmission($webform, $edit);
+
+    // Check 'access webform submission user' permission.
+    $uid = $own_submission_account->id();
+    $this->drupalGet("user/$uid/submissions");
+    $this->assertResponse(200);
+
+    // Check view own previous submission message.
+    $this->drupalGet('/webform/' . $webform->id());
+    $this->assertRaw('You have already submitted this webform.');
+    $this->assertRaw("<a href=\"{$base_path}webform/{$webform_id}/submissions/{$sid_2}\">View your previous submission</a>.");
+
+    // Check 'view own submission' permission.
+    $this->drupalGet("webform/{$webform_id}/submissions/{$sid_2}");
+    $this->assertResponse(200);
+
+    // Check 'edit own submission' permission.
+    $this->drupalGet("webform/{$webform_id}/submissions/{$sid_2}/edit");
+    $this->assertResponse(200);
+
+    // Check 'delete own submission' permission.
+    $this->drupalGet("webform/{$webform_id}/submissions/{$sid_2}/delete");
+    $this->assertResponse(200);
+
+    $sid_3 = $this->postSubmission($webform, $edit);
+
+    // Check view own previous submissions message.
+    $this->drupalGet('/webform/' . $webform->id());
+    $this->assertRaw('You have already submitted this webform.');
+    $this->assertRaw("<a href=\"{$base_path}webform/{$webform_id}/submissions\">View your previous submissions</a>");
+
+    // Check view own previous submissions.
+    $this->drupalGet("webform/{$webform_id}/submissions");
+    $this->assertResponse(200);
+    $this->assertNoLinkByHref("{$base_path}admin/structure/webform/manage/{$webform_id}/submission/{$sid_1}");
+    $this->assertLinkByHref("{$base_path}webform/{$webform_id}/submissions/{$sid_2}");
+    $this->assertLinkByHref("{$base_path}webform/{$webform_id}/submissions/{$sid_3}");
+
+    // Check webform submission allowed.
+    $this->drupalGet("/admin/structure/webform/manage/{$webform_id}/submission/{$sid_2}");
+    $this->assertResponse(200);
+
+    // Check all results access denied.
+    $this->drupalGet('/admin/structure/webform/submissions/manage');
+    $this->assertResponse(403);
+
+    // Check webform results access denied.
+    $this->drupalGet("/admin/structure/webform/manage/{$webform_id}/results/submissions");
+    $this->assertResponse(403);
+
+    /**************************************************************************/
+    // Any submission permissions.
+    /**************************************************************************/
+
+    // Login as any user.
+    $this->drupalLogin($any_submission_account);
+
+    // Check 'access webform submission user' permission.
+    $uid = $any_submission_account->id();
+    $this->drupalGet("user/$uid/submissions");
+    $this->assertResponse(200);
+
+    // Check 'access webform submission user' permission denied.
+    $uid = $own_submission_account->id();
+    $this->drupalGet("user/$uid/submissions");
+    $this->assertResponse(200);
+
+    // Check webform results access allowed.
+    $this->drupalGet("/admin/structure/webform/manage/{$webform_id}/results/submissions");
+    $this->assertResponse(200);
+    $this->assertLinkByHref("{$base_path}admin/structure/webform/manage/{$webform_id}/submission/{$sid_1}");
+    $this->assertLinkByHref("{$base_path}admin/structure/webform/manage/{$webform_id}/submission/{$sid_2}");
+    $this->assertLinkByHref("{$base_path}admin/structure/webform/manage/{$webform_id}/submission/{$sid_3}");
+
+    // Check webform submission access allowed.
+    $this->drupalGet("/admin/structure/webform/manage/{$webform_id}/submission/{$sid_2}");
+    $this->assertResponse(200);
+
+    // Check all results access allowed.
+    $this->drupalGet('/admin/structure/webform/submissions/manage');
+    $this->assertResponse(200);
+
+    /**************************************************************************/
+    // Own submission permissions (anonymous).
+    /**************************************************************************/
+
+    /** @var \Drupal\user\RoleInterface $anonymous_role */
+    $anonymous_role = Role::load('anonymous');
+    $anonymous_role->grantPermission('view own webform submission')
+      ->grantPermission('edit own webform submission')
+      ->grantPermission('delete own webform submission')
+      ->save();
+    $this->drupalLogout();
+
+    $edit = ['name' => '{name}', 'email' => 'example@example.com', 'subject' => '{subject}', 'message' => '{message}'];
+    $sid_4 = $this->postSubmission($webform, $edit);
+
+    // Check view own previous submission message.
+    $this->drupalGet('/webform/' . $webform->id());
+    $this->assertRaw('You have already submitted this webform.');
+    $this->assertRaw("<a href=\"{$base_path}webform/{$webform_id}/submissions/{$sid_4}\">View your previous submission</a>.");
+
+    // Check 'view own submission' permission.
+    $this->drupalGet("webform/{$webform_id}/submissions/{$sid_4}");
+    $this->assertResponse(200);
+
+    // Check 'edit own submission' permission.
+    $this->drupalGet("webform/{$webform_id}/submissions/{$sid_4}/edit");
+    $this->assertResponse(200);
+
+    // Check 'delete own submission' permission.
+    $this->drupalGet("webform/{$webform_id}/submissions/{$sid_4}/delete");
+    $this->assertResponse(200);
+
+    $sid_5 = $this->postSubmission($webform, $edit);
+
+    // Check view own previous submissions message.
+    $this->drupalGet('/webform/' . $webform->id());
+    $this->assertRaw('You have already submitted this webform.');
+    $this->assertRaw("<a href=\"{$base_path}webform/{$webform_id}/submissions\">View your previous submissions</a>");
+
+    // Check view own previous submissions.
+    $this->drupalGet("webform/{$webform_id}/submissions");
+    $this->assertResponse(200);
+    $this->assertLinkByHref("{$base_path}webform/{$webform_id}/submissions/{$sid_4}");
+    $this->assertLinkByHref("{$base_path}webform/{$webform_id}/submissions/{$sid_5}");
+
+    /**************************************************************************/
+    // Administer webform or webform submission permission.
+    /**************************************************************************/
+
+    $this->drupalLogin($admin_webform_account);
+    $uid = $own_submission_account->id();
+    $this->drupalGet("user/$uid/submissions");
+    $this->assertResponse(200);
+
+    $this->drupalLogin($admin_submission_account);
+    $uid = $own_submission_account->id();
+    $this->drupalGet("user/$uid/submissions");
+    $this->assertResponse(200);
+
+    // Check user can't see all submissions unless they are the owner.
+    $this->drupalLogin($own_webform_account);
+    $this->drupalGet("/admin/structure/webform/manage/{$webform_id}/results/submissions");
+    $this->assertResponse(403);
+
+    // Check user can see all submissions when they are the webform owner.
+    $webform->setOwner($own_webform_account)->save();
+    $this->drupalGet("/admin/structure/webform/manage/{$webform_id}/results/submissions");
+    $this->assertResponse(200);
+    $this->assertLinkByHref("{$base_path}admin/structure/webform/manage/{$webform_id}/submission/{$sid_1}");
+    $this->assertLinkByHref("{$base_path}admin/structure/webform/manage/{$webform_id}/submission/{$sid_2}");
+    $this->assertLinkByHref("{$base_path}admin/structure/webform/manage/{$webform_id}/submission/{$sid_3}");
+    $this->assertLinkByHref("{$base_path}admin/structure/webform/manage/{$webform_id}/submission/{$sid_4}");
+
+    // Check user can the submissions when they are the webform owner.
+    $this->drupalGet("admin/structure/webform/manage/{$webform_id}/submission/{$sid_4}");
+    $this->assertResponse(200);
+
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Block/WebformBlockContextTest.php b/web/modules/webform/src/Tests/Block/WebformBlockContextTest.php
index 1cf2ab89f18e1b9c4b41bd98df2f9c8492449321..9f2fa161493956ded9deaa6580d654083a64c777 100644
--- a/web/modules/webform/src/Tests/Block/WebformBlockContextTest.php
+++ b/web/modules/webform/src/Tests/Block/WebformBlockContextTest.php
@@ -32,7 +32,7 @@ public function setUp() {
       'node' => '@node.node_route_context:node',
     ];
     foreach ($contexts as $type => $context) {
-      $block = $this->placeBlock('webform_test_block_context_block', ['label' => '{' . $type . ' context}']);
+      $block = $this->drupalPlaceBlock('webform_test_block_context_block', ['label' => '{' . $type . ' context}']);
       $block->setVisibilityConfig('webform', [
         'id' => 'webform',
         'webforms' => ['contact' => 'contact'],
@@ -43,7 +43,7 @@ public function setUp() {
       ]);
       $block->save();
     }
-    $block = $this->placeBlock('webform_test_block_context_block', ['label' => '{all contexts}']);
+    $block = $this->drupalPlaceBlock('webform_test_block_context_block', ['label' => '{all contexts}']);
     $block->setVisibilityConfig('webform', [
       'id' => 'webform',
       'webforms' => ['contact' => 'contact'],
@@ -61,7 +61,7 @@ public function testBlockContext() {
     $webform = Webform::load('contact');
 
     // Check webform context.
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertRaw('{all contexts}');
     $this->assertRaw('{webform context}');
 
@@ -76,7 +76,7 @@ public function testBlockContext() {
     $node->webform->target_id = 'contact';
     $node->webform->status = 1;
     $node->save();
-    $this->drupalGet('node/' . $node->id());
+    $this->drupalGet('/node/' . $node->id());
     $this->assertRaw('{all contexts}');
     $this->assertRaw('{node context}');
   }
diff --git a/web/modules/webform/src/Tests/Block/WebformBlockTest.php b/web/modules/webform/src/Tests/Block/WebformBlockTest.php
index e644098ee1d2a6e69dadb5d1b057cce22036b84e..1fb130c8bae294104a77246456f982acd968fb54 100644
--- a/web/modules/webform/src/Tests/Block/WebformBlockTest.php
+++ b/web/modules/webform/src/Tests/Block/WebformBlockTest.php
@@ -35,13 +35,13 @@ public function testBlock() {
     ]);
 
     // Check contact webform.
-    $this->drupalGet('<front>');
+    $this->drupalGet('/<front>');
     $this->assertRaw('webform-submission-contact-add-form');
 
     // Check contact webform with default data.
     $block->getPlugin()->setConfigurationValue('default_data', "name: 'John Smith'");
     $block->save();
-    $this->drupalGet('<front>');
+    $this->drupalGet('/<front>');
     $this->assertRaw('webform-submission-contact-add-form');
     $this->assertFieldByName('name', 'John Smith');
 
@@ -51,11 +51,20 @@ public function testBlock() {
     $this->drupalPostForm('<front>', [], t('Submit'));
     $this->assertRaw('This is a custom inline confirmation message.');
 
-    // Check confirmation message webform.
+    // Check confirmation message webform displayed on front page.
     $block->getPlugin()->setConfigurationValue('webform_id', 'test_confirmation_message');
     $block->save();
     $this->drupalPostForm('<front>', [], t('Submit'));
     $this->assertRaw('This is a <b>custom</b> confirmation message.');
+    $this->assertUrl('/user/login');
+
+    // Check confirmation message webform display on webform URL.
+    $block->getPlugin()->setConfigurationValue('redirect', TRUE);
+    $block->save();
+    $this->drupalPostForm('<front>', [], t('Submit'));
+    $this->assertRaw('This is a <b>custom</b> confirmation message.');
+    $this->assertUrl('webform/test_confirmation_message');
+
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Composite/WebformCompositeCustomFileTest.php b/web/modules/webform/src/Tests/Composite/WebformCompositeCustomFileTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..fa28b18c0c54b93ee36486247cb3e6c91aa1cebc
--- /dev/null
+++ b/web/modules/webform/src/Tests/Composite/WebformCompositeCustomFileTest.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\webform\Tests\Composite;
+
+use Drupal\file\Entity\File;
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Entity\WebformSubmission;
+use Drupal\webform\Tests\Element\WebformElementManagedFileTestBase;
+
+/**
+ * Tests for custom composite element.
+ *
+ * @group Webform
+ */
+class WebformCompositeCustomFileTest extends WebformElementManagedFileTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_composite_custom_file'];
+
+  /**
+   * Test custom composite element.
+   */
+  public function testCustom() {
+    $webform = Webform::load('test_composite_custom_file');
+
+    $first_file = $this->files[0];
+
+    /**************************************************************************/
+
+    // Create submission with file.
+    $edit = [
+      'webform_custom_composite_file[items][0][_item_][textfield]' => '{textfield}',
+      'files[webform_custom_composite_file_items_0__item__managed_file]' => \Drupal::service('file_system')->realpath($first_file->uri),
+    ];
+    $sid = $this->postSubmission($webform, $edit);
+    $webform_submission = WebformSubmission::load($sid);
+
+    $fid = $this->getLastFileId();
+    $file = File::load($fid);
+
+    // Check file permanent.
+    $this->assert($file->isPermanent(), 'Test file is permanent');
+
+    // Check file upload.
+    $element_data = $webform_submission->getElementData('webform_custom_composite_file');
+    $this->assertEqual($element_data[0]['managed_file'], $fid, 'Test file was upload to the current submission');
+
+    // Check test file file usage.
+    $this->assertIdentical(['webform' => ['webform_submission' => [$sid => '1']]], $this->fileUsage->listUsage($file), 'The file has 1 usage.');
+
+    // Check test file uploaded file path.
+    $this->assertEqual($file->getFileUri(), 'private://webform/test_composite_custom_file/' . $sid . '/' . $first_file->filename);
+
+    // Check that test file exists.
+    $this->assert(file_exists($file->getFileUri()), 'File exists');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Composite/WebformCompositeCustomTest.php b/web/modules/webform/src/Tests/Composite/WebformCompositeCustomTest.php
index 166c3d3dbc5d6aefa5f4d5004120cbc35ae561c2..3ebfd75657bc870a87a02b4c7160183a9a47869e 100644
--- a/web/modules/webform/src/Tests/Composite/WebformCompositeCustomTest.php
+++ b/web/modules/webform/src/Tests/Composite/WebformCompositeCustomTest.php
@@ -25,12 +25,13 @@ public function testCustom() {
 
     /* Display */
 
-    $this->drupalGet('webform/test_composite_custom');
+    $this->drupalGet('/webform/test_composite_custom');
 
     // Check basic custom composite.
-    $this->assertRaw('<label for="edit-webform-custom-composite-basic">webform_custom_composite_basic</label>');
-    $this->assertRaw('<div id="webform_custom_composite_basic_table" class="webform-multiple-table webform-multiple-table-responsive">');
-    $this->assertRaw('<th class="webform_custom_composite_basic-table--handle webform-multiple-table--handle"></th>');
+    $this->assertRaw('<label>webform_custom_composite_basic</label>');
+    $this->assertRaw('<div id="webform_custom_composite_basic_table">');
+    $this->assertRaw('<div class="webform-multiple-table webform-multiple-table-responsive">');
+    $this->assertRaw('<th class="webform_custom_composite_basic-table--handle webform-multiple-table--handle"><span class="visually-hidden">Re-order</span></th>');
     $this->assertRaw('<th class="webform_custom_composite_basic-table--first_name webform-multiple-table--first_name">First name</th>');
     $this->assertRaw('<th class="webform_custom_composite_basic-table--last_name webform-multiple-table--last_name">Last name</th>');
     $this->assertRaw('<th class="webform_custom_composite_basic-table--weight webform-multiple-table--weight">Weight</th>');
diff --git a/web/modules/webform/src/Tests/Composite/WebformCompositeFormatTest.php b/web/modules/webform/src/Tests/Composite/WebformCompositeFormatTest.php
index 059e2551d4ddc7af7a968021d3e4aba0c9f5e965..13e4bde78c75f5f493a922fcfeb7293c70790eae 100644
--- a/web/modules/webform/src/Tests/Composite/WebformCompositeFormatTest.php
+++ b/web/modules/webform/src/Tests/Composite/WebformCompositeFormatTest.php
@@ -20,7 +20,7 @@ class WebformCompositeFormatTest extends WebformTestBase {
    *
    * @var array
    */
-  public static $modules = ['filter', 'webform'];
+  public static $modules = ['filter', 'address', 'webform'];
 
   /**
    * Webforms to load.
@@ -50,38 +50,26 @@ public function testFormat() {
       'Likert (Value)' => '<div class="item-list"><ul><li><b>Please answer question 1?:</b> 1</li><li><b>How about now answering question 2?:</b> 1</li><li><b>Finally, here is question 3?:</b> 1</li></ul></div>',
       'Likert (Raw value)' => '<div class="item-list"><ul><li><b>q1:</b> 1</li><li><b>q2:</b> 1</li><li><b>q3:</b> 1</li></ul></div>',
       'Likert (List)' => '<div class="item-list"><ul><li><b>Please answer question 1?:</b> 1</li><li><b>How about now answering question 2?:</b> 1</li><li><b>Finally, here is question 3?:</b> 1</li></ul></div>',
-      'Address (Value)' => '10 Main Street<br />10 Main Street<br />Springfield, Alabama. Loremipsum<br />Afghanistan<br />',
-      'Address (Raw value)' => '<div class="item-list"><ul><li><b>address:</b> 10 Main Street</li><li><b>address_2:</b> 10 Main Street</li><li><b>city:</b> Springfield</li><li><b>state_province:</b> Alabama</li><li><b>postal_code:</b> Loremipsum</li><li><b>country:</b> Afghanistan</li></ul></div><br /><br />',
-      'Address (List)' => '<div class="item-list"><ul><li><b>Address:</b> 10 Main Street</li><li><b>Address 2:</b> 10 Main Street</li><li><b>City/Town:</b> Springfield</li><li><b>State/Province:</b> Alabama</li><li><b>Zip/Postal Code:</b> Loremipsum</li><li><b>Country:</b> Afghanistan</li></ul></div><br /><br />',
+      'Basic address (Value)' => '10 Main Street<br />10 Main Street<br />Springfield, Alabama. 11111<br />Afghanistan<br />',
+      'Basic address (Raw value)' => '<div class="item-list"><ul><li><b>address:</b> 10 Main Street</li><li><b>address_2:</b> 10 Main Street</li><li><b>city:</b> Springfield</li><li><b>state_province:</b> Alabama</li><li><b>postal_code:</b> 11111</li><li><b>country:</b> Afghanistan</li></ul></div><br /><br />',
+      'Basic address (List)' => '<div class="item-list"><ul><li><b>Address:</b> 10 Main Street</li><li><b>Address 2:</b> 10 Main Street</li><li><b>City/Town:</b> Springfield</li><li><b>State/Province:</b> Alabama</li><li><b>ZIP/Postal Code:</b> 11111</li><li><b>Country:</b> Afghanistan</li></ul></div><br /><br />',
+      'Advanced address (Value)' => '<div class="address" translate="no"><span class="given-name">John</span> <span class="family-name">Smith</span><br>
+<span class="organization">Google Inc.</span><br>
+<span class="address-line1">1098 Alta Ave</span><br>
+<span class="locality">Mountain View</span>, <span class="administrative-area">CA</span> <span class="postal-code">94043</span><br>
+<span class="country">United States</span></div>',
+      'Advanced address (Raw value)' => '<div class="item-list"><ul><li><b>given_name:</b> John</li><li><b>family_name:</b> Smith</li><li><b>organization:</b> Google Inc.</li><li><b>address_line1:</b> 1098 Alta Ave</li><li><b>postal_code:</b> 94043</li><li><b>locality:</b> Mountain View</li><li><b>administrative_area:</b> CA</li><li><b>country_code:</b> US</li><li><b>langcode:</b> en</li></ul>',
+      'Advanced address (List)' => '<div class="item-list"><ul><li><b>Given name:</b> John</li><li><b>Family name:</b> Smith</li><li><b>Organization:</b> Google Inc.</li><li><b>Address line 1:</b> 1098 Alta Ave</li><li><b>Postal code:</b> 94043</li><li><b>Locality:</b> Mountain View</li><li><b>Administrative area:</b> CA</li><li><b>Country code:</b> US</li><li><b>Language code:</b> en</li></ul>',
       'Link (Value)' => '<a href="http://example.com">Loremipsum</a>',
     ];
     foreach ($elements as $label => $value) {
-      $this->assertContains($body, '<b>' . $label . '</b><br />' . $value, new FormattableMarkup('Found @label: @value', ['@label' => $label, '@value' => $value]));
+      $this->assertContains('<b>' . $label . '</b><br />' . $value, $body, new FormattableMarkup('Found @label: @value', ['@label' => $label, '@value' => $value]));
     }
 
     // Check composite elements formatted as text.
     $body = $this->getMessageBody($submission, 'email_text');
     $elements = [
       'Link (Value): Loremipsum (http://example.com)',
-      'Address (Value):
-10 Main Street
-10 Main Street
-Springfield, Alabama. Loremipsum
-Afghanistan',
-      'Address (Raw value):
-address: 10 Main Street
-address_2: 10 Main Street
-city: Springfield
-state_province: Alabama
-postal_code: Loremipsum
-country: Afghanistan',
-      'Address (List):
-Address: 10 Main Street
-Address 2: 10 Main Street
-City/Town: Springfield
-State/Province: Alabama
-Zip/Postal Code: Loremipsum
-Country: Afghanistan',
       'Likert (Value):
 Please answer question 1?: 1
 How about now answering question 2?: 1
@@ -98,9 +86,54 @@ public function testFormat() {
 Please answer question 1?: 1
 How about now answering question 2?: 1
 Finally, here is question 3?: 1',
+      'Basic address (Value):
+10 Main Street
+10 Main Street
+Springfield, Alabama. 11111
+Afghanistan',
+      'Basic address (Raw value):
+address: 10 Main Street
+address_2: 10 Main Street
+city: Springfield
+state_province: Alabama
+postal_code: 11111
+country: Afghanistan',
+      'Basic address (List):
+Address: 10 Main Street
+Address 2: 10 Main Street
+City/Town: Springfield
+State/Province: Alabama
+ZIP/Postal Code: 11111
+Country: Afghanistan',
+      'Advanced address (Value):
+John Smith
+Google Inc.
+1098 Alta Ave
+Mountain View, CA 94043
+United States',
+      'Advanced address (Raw value):
+given_name: John
+family_name: Smith
+organization: Google Inc.
+address_line1: 1098 Alta Ave
+postal_code: 94043
+locality: Mountain View
+administrative_area: CA
+country_code: US
+langcode: en',
+      'Advanced address (List):
+Given name: John
+Family name: Smith
+Organization: Google Inc.
+Address line 1: 1098 Alta Ave
+Postal code: 94043
+Locality: Mountain View
+Administrative area: CA
+Country code: US
+Language code: en',
     ];
     foreach ($elements as $value) {
-      $this->assertContains($body, $value, new FormattableMarkup('Found @value', ['@value' => $value]));
+      $this->assertContains($value, $body, new FormattableMarkup('Found @value', ['@value' => $value]));
     }
 
     /**************************************************************************/
@@ -121,62 +154,62 @@ public function testFormat() {
     $this->debug($body);
 
     $elements = [
-      'Address (Ordered list)' => '<div class="item-list"><ol><li>10 Main Street<br />10 Main Street<br />Springfield, Alabama. Loremipsum<br />Afghanistan</li><li>10 Main Street<br />10 Main Street<br />Springfield, Alabama. Loremipsum<br />Afghanistan</li><li>10 Main Street<br />10 Main Street<br />Springfield, Alabama. Loremipsum<br />Afghanistan</li></ol></div>',
-      'Address (Unordered list)' => '<div class="item-list"><ul><li>10 Main Street<br />10 Main Street<br />Springfield, Alabama. Loremipsum<br />Afghanistan</li><li>10 Main Street<br />10 Main Street<br />Springfield, Alabama. Loremipsum<br />Afghanistan</li><li>10 Main Street<br />10 Main Street<br />Springfield, Alabama. Loremipsum<br />Afghanistan</li></ul></div>',
-      'Address (Horizontal rule)' => '10 Main Street<br />10 Main Street<br />Springfield, Alabama. Loremipsum<br />Afghanistan<hr class="webform-horizontal-rule" />10 Main Street<br />10 Main Street<br />Springfield, Alabama. Loremipsum<br />Afghanistan<hr class="webform-horizontal-rule" />10 Main Street<br />10 Main Street<br />Springfield, Alabama. Loremipsum<br />Afghanistan',
-      'Address (Table)' => '<table width="100%" cellspacing="0" cellpadding="5" border="1" class="responsive-enabled" data-striping="1"><thead><tr><th bgcolor="#eee">Address</th><th bgcolor="#eee">Address 2</th><th bgcolor="#eee">City/Town</th><th bgcolor="#eee">State/Province</th><th bgcolor="#eee">Zip/Postal Code</th><th bgcolor="#eee">Country</th></tr></thead><tbody><tr class="odd"><td>10 Main Street</td><td>10 Main Street</td><td>Springfield</td><td>Alabama</td><td>Loremipsum</td><td>Afghanistan</td></tr><tr class="even"><td>10 Main Street</td><td>10 Main Street</td><td>Springfield</td><td>Alabama</td><td>Loremipsum</td><td>Afghanistan</td></tr><tr class="odd"><td>10 Main Street</td><td>10 Main Street</td><td>Springfield</td><td>Alabama</td><td>Loremipsum</td><td>Afghanistan</td></tr></tbody></table>',
+      'Basic address (Ordered list)' => '<div class="item-list"><ol><li>10 Main Street<br />10 Main Street<br />Springfield, Alabama. 11111<br />Afghanistan</li><li>10 Main Street<br />10 Main Street<br />Springfield, Alabama. 11111<br />Afghanistan</li><li>10 Main Street<br />10 Main Street<br />Springfield, Alabama. 11111<br />Afghanistan</li></ol></div>',
+      'Basic address (Unordered list)' => '<div class="item-list"><ul><li>10 Main Street<br />10 Main Street<br />Springfield, Alabama. 11111<br />Afghanistan</li><li>10 Main Street<br />10 Main Street<br />Springfield, Alabama. 11111<br />Afghanistan</li><li>10 Main Street<br />10 Main Street<br />Springfield, Alabama. 11111<br />Afghanistan</li></ul></div>',
+      'Basic address (Horizontal rule)' => '10 Main Street<br />10 Main Street<br />Springfield, Alabama. 11111<br />Afghanistan<hr class="webform-horizontal-rule" />10 Main Street<br />10 Main Street<br />Springfield, Alabama. 11111<br />Afghanistan<hr class="webform-horizontal-rule" />10 Main Street<br />10 Main Street<br />Springfield, Alabama. 11111<br />Afghanistan',
+      'Basic address (Table)' => '<table width="100%" cellspacing="0" cellpadding="5" border="1" class="responsive-enabled" data-striping="1"><thead><tr><th bgcolor="#eee">Address</th><th bgcolor="#eee">Address 2</th><th bgcolor="#eee">City/Town</th><th bgcolor="#eee">State/Province</th><th bgcolor="#eee">ZIP/Postal Code</th><th bgcolor="#eee">Country</th></tr></thead><tbody><tr class="odd"><td>10 Main Street</td><td>10 Main Street</td><td>Springfield</td><td>Alabama</td><td>11111</td><td>Afghanistan</td></tr><tr class="even"><td>10 Main Street</td><td>10 Main Street</td><td>Springfield</td><td>Alabama</td><td>11111</td><td>Afghanistan</td></tr><tr class="odd"><td>10 Main Street</td><td>10 Main Street</td><td>Springfield</td><td>Alabama</td><td>11111</td><td>Afghanistan</td></tr></tbody></table>',
     ];
     foreach ($elements as $label => $value) {
-      $this->assertContains($body, '<b>' . $label . '</b><br />' . $value, new FormattableMarkup('Found @label: @value', ['@label' => $label, '@value' => $value]));
+      $this->assertContains('<b>' . $label . '</b><br />' . $value, $body, new FormattableMarkup('Found @label: @value', ['@label' => $label, '@value' => $value]));
     }
 
     // Check composite elements formatted as text.
     $body = $this->getMessageBody($submission, 'email_text');
     $elements = [
-      'Address (Ordered list):
+      'Basic address (Ordered list):
 1. 10 Main Street
    10 Main Street
-   Springfield, Alabama. Loremipsum
+   Springfield, Alabama. 11111
    Afghanistan
 2. 10 Main Street
    10 Main Street
-   Springfield, Alabama. Loremipsum
+   Springfield, Alabama. 11111
    Afghanistan
 3. 10 Main Street
    10 Main Street
-   Springfield, Alabama. Loremipsum
+   Springfield, Alabama. 11111
    Afghanistan',
-      'Address (Unordered list):
+      'Basic address (Unordered list):
 - 10 Main Street
   10 Main Street
-  Springfield, Alabama. Loremipsum
+  Springfield, Alabama. 11111
   Afghanistan
 - 10 Main Street
   10 Main Street
-  Springfield, Alabama. Loremipsum
+  Springfield, Alabama. 11111
   Afghanistan
 - 10 Main Street
   10 Main Street
-  Springfield, Alabama. Loremipsum
+  Springfield, Alabama. 11111
   Afghanistan',
-      'Address (Horizontal rule):
+      'Basic address (Horizontal rule):
 10 Main Street
 10 Main Street
-Springfield, Alabama. Loremipsum
+Springfield, Alabama. 11111
 Afghanistan
 ---
 10 Main Street
 10 Main Street
-Springfield, Alabama. Loremipsum
+Springfield, Alabama. 11111
 Afghanistan
 ---
 10 Main Street
 10 Main Street
-Springfield, Alabama. Loremipsum
+Springfield, Alabama. 11111
 Afghanistan',
     ];
     foreach ($elements as $value) {
-      $this->assertContains($body, $value, new FormattableMarkup('Found @value', ['@value' => $value]));
+      $this->assertContains($value, $body, new FormattableMarkup('Found @value', ['@value' => $value]));
     }
   }
 
diff --git a/web/modules/webform/src/Tests/Composite/WebformCompositePluginFileTest.php b/web/modules/webform/src/Tests/Composite/WebformCompositePluginFileTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ff9da7522a3de8a268a4fe91496e95d8f1a3f215
--- /dev/null
+++ b/web/modules/webform/src/Tests/Composite/WebformCompositePluginFileTest.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Drupal\webform\Tests\Composite;
+
+use Drupal\file\Entity\File;
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Entity\WebformSubmission;
+use Drupal\webform\Tests\Element\WebformElementManagedFileTestBase;
+
+/**
+ * Tests for composite plugin file upload.
+ *
+ * @group Webform
+ */
+class WebformCompositePluginFileTest extends WebformElementManagedFileTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform_test_element'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_comp_file_plugin'];
+
+  /**
+   * Test composite plugin.
+   */
+  public function testPlugin() {
+    $webform = Webform::load('test_element_comp_file_plugin');
+
+    $first_file = $this->files[0];
+    $second_file = $this->files[1];
+
+    /**************************************************************************/
+    // Single composite with file upload.
+    /**************************************************************************/
+
+    // Create submission with file.
+    $edit = [
+      'webform_test_composite_file[textfield]' => '{textfield}',
+      'files[webform_test_composite_file_managed_file]' => \Drupal::service('file_system')->realpath($first_file->uri),
+    ];
+    $sid = $this->postSubmission($webform, $edit);
+    $webform_submission = WebformSubmission::load($sid);
+
+    $fid = $this->getLastFileId();
+    $file = File::load($fid);
+
+    // Check file permanent.
+    $this->assert($file->isPermanent(), 'Test file is permanent');
+
+    // Check file upload.
+    $element_data = $webform_submission->getElementData('webform_test_composite_file');
+    $this->assertEqual($element_data['managed_file'], $fid, 'Test file was upload to the current submission');
+
+    // Check test file file usage.
+    $this->assertIdentical(['webform' => ['webform_submission' => [$sid => '1']]], $this->fileUsage->listUsage($file), 'The file has 1 usage.');
+
+    // Check test file uploaded file path.
+    $this->assertEqual($file->getFileUri(), 'private://webform/test_element_comp_file_plugin/' . $sid . '/' . $first_file->filename);
+
+    // Check that test file exists.
+    $this->assert(file_exists($file->getFileUri()), 'File exists');
+
+    /**************************************************************************/
+    // Multiple composite with file upload.
+    /**************************************************************************/
+
+    // Create submission with file.
+    $edit = [
+      'webform_test_composite_file_multiple_header[items][0][textfield]' => '{textfield}',
+      'files[webform_test_composite_file_multiple_header_items_0_managed_file]' => \Drupal::service('file_system')->realpath($second_file->uri),
+    ];
+    $sid = $this->postSubmission($webform, $edit);
+    $webform_submission = WebformSubmission::load($sid);
+
+    $fid = $this->getLastFileId();
+    $file = File::load($fid);
+
+    // Check file permanent.
+    $this->assert($file->isPermanent(), 'Test file is permanent');
+
+    // Check file upload.
+    $element_data = $webform_submission->getElementData('webform_test_composite_file_multiple_header');
+    $this->assertEqual($element_data[0]['managed_file'], $fid, 'Test file was upload to the current submission');
+
+    // Check test file file usage.
+    $this->assertIdentical(['webform' => ['webform_submission' => [$sid => '1']]], $this->fileUsage->listUsage($file), 'The file has 1 usage.');
+
+    // Check test file uploaded file path.
+    $this->assertEqual($file->getFileUri(), 'private://webform/test_element_comp_file_plugin/' . $sid . '/' . $second_file->filename);
+
+    // Check that test file exists.
+    $this->assert(file_exists($file->getFileUri()), 'File exists');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Composite/WebformCompositePluginTest.php b/web/modules/webform/src/Tests/Composite/WebformCompositePluginTest.php
index 953e69132da3e1e33d8b9c1e80f3a4a2aa8d653a..f71b51b7fcba7d48e634209646528b615e137863 100644
--- a/web/modules/webform/src/Tests/Composite/WebformCompositePluginTest.php
+++ b/web/modules/webform/src/Tests/Composite/WebformCompositePluginTest.php
@@ -32,10 +32,10 @@ public function testPlugin() {
 
     /* Display */
 
-    $this->drupalGet('webform/test_element_composite_plugin');
+    $this->drupalGet('/webform/test_element_composite_plugin');
 
     // Check fieldset with nested elements is rendered.
-    $this->assertRaw('<fieldset data-drupal-selector="edit-webform-test-composite-fieldset" id="edit-webform-test-composite-fieldset" class="js-form-item form-item js-form-wrapper form-wrapper">');
+    $this->assertRaw('<fieldset data-drupal-selector="edit-webform-test-composite-fieldset" id="edit-webform-test-composite-fieldset" class="js-webform-type-fieldset webform-type-fieldset js-form-item form-item js-form-wrapper form-wrapper">');
     $this->assertRaw('<span class="fieldset-legend">fieldset</span>');
 
     /* Processing */
@@ -61,7 +61,7 @@ public function testPlugin() {
       'webform_test_composite[datetime][time]' => '23:19:25',
       'webform_test_composite[nested_tel]' => '123-456-7890',
       'webform_test_composite[nested_select]' => 'Monday',
-      'webform_test_composite[nested_radios]' => 'Monday'
+      'webform_test_composite[nested_radios]' => 'Monday',
     ];
     $this->drupalPostForm('webform/test_element_composite_plugin', $edit, t('Submit'));
     $this->assertRaw("webform_test_composite:
diff --git a/web/modules/webform/src/Tests/Composite/WebformCompositeTest.php b/web/modules/webform/src/Tests/Composite/WebformCompositeTest.php
index 729ba9c8f4ee7ca56e5767337fd17e293bdaf3d0..f0198a729e438c8c83ee9e85afe12df338d4367a 100644
--- a/web/modules/webform/src/Tests/Composite/WebformCompositeTest.php
+++ b/web/modules/webform/src/Tests/Composite/WebformCompositeTest.php
@@ -25,10 +25,11 @@ public function testComposite() {
 
     /* Display */
 
-    $this->drupalGet('webform/test_composite');
+    $this->drupalGet('/webform/test_composite');
 
     // Check webform contact basic.
-    $this->assertRaw('<div id="edit-contact-basic--wrapper" class="form-composite js-form-item form-item js-form-type-webform-contact form-type-webform-contact js-form-item-contact-basic form-item-contact-basic form-no-label">');
+    $this->assertRaw('<fieldset data-drupal-selector="edit-contact-basic" id="edit-contact-basic--wrapper" class="webform-contact--wrapper fieldgroup form-composite webform-composite-hidden-title required js-webform-type-webform-contact webform-type-webform-contact js-form-item form-item js-form-wrapper form-wrapper">');
+    $this->assertRaw('<span class="visually-hidden fieldset-legend js-form-required form-required">Contact basic</span>');
     $this->assertRaw('<label for="edit-contact-basic-name" class="js-form-required form-required">Name</label>');
     $this->assertRaw('<input data-drupal-selector="edit-contact-basic-name" type="text" id="edit-contact-basic-name" name="contact_basic[name]" value="John Smith" size="60" maxlength="255" class="form-text required" required="required" aria-required="true" />');
 
@@ -45,9 +46,9 @@ public function testComposite() {
     $this->assertNoRaw('edit-contact-advanced-country');
 
     // Check link multiple in table.
-    $this->assertRaw('<label for="edit-link-multiple">Link multiple</label>');
-    $this->assertRaw('<th class="link_multiple-table--title webform-multiple-table--title">Link Title<a href="#help" title="This is link title help" data-webform-help="This is link title help" class="webform-element-help">?</a>');
-    $this->assertRaw('<th class="link_multiple-table--url webform-multiple-table--url">Link URL<a href="#help" title="This is link url help" data-webform-help="This is link url help" class="webform-element-help">?</a>');
+    $this->assertRaw('<label>Link multiple</label>');
+    $this->assertRaw('<th class="link_multiple-table--title webform-multiple-table--title">Link Title<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;Link Title&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;This is link title help&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
+    $this->assertRaw('<th class="link_multiple-table--url webform-multiple-table--url">Link URL<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;Link URL&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;This is link url help&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
 
     /* Processing */
 
diff --git a/web/modules/webform/src/Tests/Element/WebformElementAccessTest.php b/web/modules/webform/src/Tests/Element/WebformElementAccessTest.php
index 8202987069e4c1b8ef153a928dc5ddfac67ff327..14dbc737317c64494fee54e2955bf2089ae15c6d 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementAccessTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementAccessTest.php
@@ -26,66 +26,78 @@ class WebformElementAccessTest extends WebformElementTestBase {
    */
   protected static $testWebforms = ['test_element_access'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
   /**
    * Test element access.
    */
   public function testAccess() {
+    $normal_user = $this->drupalCreateUser([
+      'access user profiles',
+    ]);
+
+    $admin_submission_user = $this->drupalCreateUser([
+      'access user profiles',
+      'administer webform submission',
+    ]);
+
+    $own_submission_user = $this->drupalCreateUser([
+      'access user profiles',
+      'access webform overview',
+      'create webform',
+      'edit own webform',
+      'delete own webform',
+      'view own webform submission',
+      'edit own webform submission',
+      'delete own webform submission',
+    ]);
+
     $webform = Webform::load('test_element_access');
 
+    /**************************************************************************/
+
     // Check user from USER:1 to admin submission user.
     $elements = $webform->get('elements');
-    $elements = str_replace('      - 1', '      - ' . $this->adminSubmissionUser->id(), $elements);
-    $elements = str_replace('USER:1', 'USER:' . $this->adminSubmissionUser->id(), $elements);
+    $elements = str_replace('      - 1', '      - ' . $admin_submission_user->id(), $elements);
+    $elements = str_replace('USER:1', 'USER:' . $admin_submission_user->id(), $elements);
     $webform->set('elements', $elements);
     $webform->save();
 
     // Create a webform submission.
-    $this->drupalLogin($this->normalUser);
+    $this->drupalLogin($normal_user);
     $sid = $this->postSubmission($webform);
     $webform_submission = WebformSubmission::load($sid);
 
     // Check admins have 'administer webform element access' permission.
     $this->drupalLogin($this->rootUser);
-    $this->drupalGet('admin/structure/webform/manage/test_element_access/element/access_create_roles_anonymous/edit');
+    $this->drupalGet('/admin/structure/webform/manage/test_element_access/element/access_create_roles_anonymous/edit');
     $this->assertFieldById('edit-properties-access-create-roles-anonymous');
 
     // Check webform builder don't have 'administer webform element access'
     // permission.
-    $this->drupalLogin($this->ownWebformUser);
-    $this->drupalGet('admin/structure/webform/manage/test_element_access/element/access_create_roles_anonymous/edit');
+    $this->drupalLogin($own_submission_user);
+    $this->drupalGet('/admin/structure/webform/manage/test_element_access/element/access_create_roles_anonymous/edit');
     $this->assertNoFieldById('edit-properties-access-create-roles-anonymous');
 
     /* Create access */
 
     // Check anonymous role access.
     $this->drupalLogout();
-    $this->drupalGet('webform/test_element_access');
+    $this->drupalGet('/webform/test_element_access');
     $this->assertFieldByName('access_create_roles_anonymous');
     $this->assertNoFieldByName('access_create_roles_authenticated');
     $this->assertNoFieldByName('access_create_users');
     $this->assertNoFieldByName('access_create_permissions');
 
     // Check authenticated access.
-    $this->drupalLogin($this->normalUser);
-    $this->drupalGet('webform/test_element_access');
+    $this->drupalLogin($normal_user);
+    $this->drupalGet('/webform/test_element_access');
     $this->assertNoFieldByName('access_create_roles_anonymous');
     $this->assertFieldByName('access_create_roles_authenticated');
     $this->assertNoFieldByName('access_create_users');
     $this->assertFieldByName('access_create_permissions');
 
     // Check admin user access.
-    $this->drupalLogin($this->adminSubmissionUser);
-    $this->drupalGet('webform/test_element_access');
+    $this->drupalLogin($admin_submission_user);
+    $this->drupalGet('/webform/test_element_access');
     $this->assertNoFieldByName('access_create_roles_anonymous');
     $this->assertFieldByName('access_create_roles_authenticated');
     $this->assertFieldByName('access_create_users');
@@ -102,7 +114,7 @@ public function testAccess() {
     $this->assertNoFieldByName('access_update_permissions');
 
     // Check authenticated role access.
-    $this->drupalLogin($this->normalUser);
+    $this->drupalLogin($normal_user);
     $this->drupalGet("/webform/test_element_access/submissions/$sid/edit");
     $this->assertNoFieldByName('access_update_roles_anonymous');
     $this->assertFieldByName('access_update_roles_authenticated');
@@ -110,7 +122,7 @@ public function testAccess() {
     $this->assertFieldByName('access_update_permissions');
 
     // Check admin user access.
-    $this->drupalLogin($this->adminSubmissionUser);
+    $this->drupalLogin($admin_submission_user);
     $this->drupalGet("/admin/structure/webform/manage/test_element_access/submission/$sid/edit");
     $this->assertNoFieldByName('access_update_roles_anonymous');
     $this->assertFieldByName('access_update_roles_authenticated');
@@ -133,7 +145,7 @@ public function testAccess() {
       $this->drupalGet($url['path'], $url['options']);
       $this->assertRaw('access_view_roles (anonymous)');
       $this->assertNoRaw('access_view_roles (authenticated)');
-      $this->assertNoRaw('access_view_users (USER:' . $this->adminSubmissionUser->id() . ')');
+      $this->assertNoRaw('access_view_users (USER:' . $admin_submission_user->id() . ')');
       $this->assertNoRaw('access_view_permissions (access user profiles)');
 
       // Check authenticated role access.
@@ -141,15 +153,15 @@ public function testAccess() {
       $this->drupalGet($url['path'], $url['options']);
       $this->assertNoRaw('access_view_roles (anonymous)');
       $this->assertRaw('access_view_roles (authenticated)');
-      $this->assertNoRaw('access_view_users (USER:' . $this->adminSubmissionUser->id() . ')');
+      $this->assertNoRaw('access_view_users (USER:' . $admin_submission_user->id() . ')');
       $this->assertRaw('access_view_permissions (access user profiles)');
 
       // Check admin user access.
-      $this->drupalLogin($this->adminSubmissionUser);
+      $this->drupalLogin($admin_submission_user);
       $this->drupalGet($url['path'], $url['options']);
       $this->assertNoRaw('access_view_roles (anonymous)');
       $this->assertRaw('access_view_roles (authenticated)');
-      $this->assertRaw('access_view_users (USER:' . $this->adminSubmissionUser->id() . ')');
+      $this->assertRaw('access_view_users (USER:' . $admin_submission_user->id() . ')');
       $this->assertRaw('access_view_permissions (access user profiles)');
     }
 
@@ -172,7 +184,7 @@ public function testAccess() {
       $this->assertNoRaw($raw, 'Anonymous user can not access token');
 
       // Check authenticated role access.
-      $this->drupalLogin($this->normalUser);
+      $this->drupalLogin($normal_user);
       $this->drupalGet($url['path'], $url['options']);
       $this->assertNoRaw($raw, 'Authenticated user can not access token');
 
@@ -182,7 +194,7 @@ public function testAccess() {
       $this->assertRaw($raw, 'Admin webform user can access token');
 
       // Check admin submission access.
-      $this->drupalLogin($this->adminSubmissionUser);
+      $this->drupalLogin($admin_submission_user);
       $this->drupalGet($url['path'], $url['options']);
       $this->assertRaw($raw, 'Admin submission user can access token');
     }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementActionsTest.php b/web/modules/webform/src/Tests/Element/WebformElementActionsTest.php
index 1ef08c84c19ba9dfdaacf26c719a316f905a24d2..c0da1e048bfb3bb094411c91d042f97614e45de7 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementActionsTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementActionsTest.php
@@ -23,13 +23,13 @@ public function testActions() {
     /* Test webform actions */
 
     // Get form.
-    $this->drupalGet('webform/test_element_actions');
+    $this->drupalGet('/webform/test_element_actions');
 
     // Check custom actions.
     $this->assertRaw('<div style="border: 2px solid red; padding: 10px" data-drupal-selector="edit-actions-custom" class="form-actions webform-actions js-form-wrapper form-wrapper" id="edit-actions-custom">');
-    $this->assertRaw('<input class="webform-button--draft js-webform-novalidate custom-draft button js-form-submit form-submit" style="font-weight: bold" data-custom-draft data-drupal-selector="edit-actions-custom-draft" type="submit" id="edit-actions-custom-draft" name="op" value="{Custom draft}" />');
+    $this->assertRaw('<input formnovalidate="formnovalidate" class="webform-button--draft custom-draft button js-form-submit form-submit" style="font-weight: bold" data-custom-draft data-drupal-selector="edit-actions-custom-draft" type="submit" id="edit-actions-custom-draft" name="op" value="{Custom draft}" />');
     $this->assertRaw('<input class="webform-button--next custom-wizard-next button js-form-submit form-submit" style="font-weight: bold" data-custom-wizard-next data-drupal-selector="edit-actions-custom-wizard-next" type="submit" id="edit-actions-custom-wizard-next" name="op" value="{Custom wizard next}" />');
-    $this->assertRaw('<input class="webform-button--reset js-webform-novalidate custom-reet button js-form-submit form-submit" style="font-weight: bold" data-custom-reset data-drupal-selector="edit-actions-custom-reset" type="submit" id="edit-actions-custom-reset" name="op" value="{Custom reset}" />');
+    $this->assertRaw('<input formnovalidate="formnovalidate" class="webform-button--reset custom-reet button js-form-submit form-submit" style="font-weight: bold" data-custom-reset data-drupal-selector="edit-actions-custom-reset" type="submit" id="edit-actions-custom-reset" name="op" value="{Custom reset}" />');
 
     // Check wizard next.
     $this->assertRaw('id="edit-actions-wizard-next-wizard-next"');
@@ -60,24 +60,24 @@ public function testActions() {
     /* Test actions buttons */
     $this->drupalLogin($this->rootUser);
 
-    $this->drupalGet('webform/test_element_actions_buttons');
+    $this->drupalGet('/webform/test_element_actions_buttons');
 
     // Check draft button.
-    $this->assertRaw('<input class="webform-button--draft js-webform-novalidate draft_button_attributes button js-form-submit form-submit" style="color: blue" data-drupal-selector="edit-actions-draft" type="submit" id="edit-actions-draft" name="op" value="Save Draft" />');
+    $this->assertRaw('<input formnovalidate="formnovalidate" class="webform-button--draft draft_button_attributes button js-form-submit form-submit" style="color: blue" data-drupal-selector="edit-actions-draft" type="submit" id="edit-actions-draft" name="op" value="Save Draft" />');
     // Check next button.
     $this->assertRaw('<input class="webform-button--next wizard_next_button_attributes button js-form-submit form-submit" style="color: yellow" data-drupal-selector="edit-actions-wizard-next" type="submit" id="edit-actions-wizard-next" name="op" value="Next Page &gt;" />');
 
     $this->drupalPostForm('webform/test_element_actions_buttons', [], t('Next Page >'));
 
     // Check previous button.
-    $this->assertRaw('<input class="webform-button--previous js-webform-novalidate wizard_prev_button_attributes button js-form-submit form-submit" style="color: yellow" data-drupal-selector="edit-actions-wizard-prev" type="submit" id="edit-actions-wizard-prev" name="op" value="&lt; Previous Page" />');
+    $this->assertRaw('<input formnovalidate="formnovalidate" class="webform-button--previous wizard_prev_button_attributes button js-form-submit form-submit" style="color: yellow" data-drupal-selector="edit-actions-wizard-prev" type="submit" id="edit-actions-wizard-prev" name="op" value="&lt; Previous Page" />');
     // Check preview button.
     $this->assertRaw('<input class="webform-button--preview preview_next_button_attributes button js-form-submit form-submit" style="color: orange" data-drupal-selector="edit-actions-preview-next" type="submit" id="edit-actions-preview-next" name="op" value="Preview" />');
 
     $this->drupalPostForm(NULL, [], t('Preview'));
 
     // Check previous button.
-    $this->assertRaw('<input class="webform-button--previous js-webform-novalidate preview_prev_button_attributes button js-form-submit form-submit" style="color: orange" data-drupal-selector="edit-actions-preview-prev" type="submit" id="edit-actions-preview-prev" name="op" value="&lt; Previous" />');
+    $this->assertRaw('<input formnovalidate="formnovalidate" class="webform-button--previous preview_prev_button_attributes button js-form-submit form-submit" style="color: orange" data-drupal-selector="edit-actions-preview-prev" type="submit" id="edit-actions-preview-prev" name="op" value="&lt; Previous" />');
     // Check submit button.
     $this->assertRaw('<input class="webform-button--submit form_submit_attributes button button--primary js-form-submit form-submit" style="color: green" data-drupal-selector="edit-actions-submit" type="submit" id="edit-actions-submit" name="op" value="Submit" />');
 
diff --git a/web/modules/webform/src/Tests/Element/WebformElementAddressTest.php b/web/modules/webform/src/Tests/Element/WebformElementAddressTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0c106609f2d8adf4365739a966819d7c12a6d470
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementAddressTest.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\webform\Entity\Webform;
+
+/**
+ * Tests for webform address element.
+ *
+ * @group Webform
+ */
+class WebformElementAddressTest extends WebformElementTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'address', 'node'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_address'];
+
+  /**
+   * Tests address element.
+   */
+  public function testAddress() {
+    $this->drupalLogin($this->rootUser);
+
+    $webform = Webform::load('test_element_address');
+
+    /**************************************************************************/
+    // Rendering.
+    /**************************************************************************/
+
+    $this->drupalGet('/webform/test_element_address');
+
+    // Check basic fieldset wrapper.
+    $this->assertRaw('<fieldset data-drupal-selector="edit-address" id="edit-address--wrapper" class="address--wrapper fieldgroup form-composite webform-composite-hidden-title js-webform-type-address webform-type-address js-form-item form-item js-form-wrapper form-wrapper">');
+    $this->assertRaw('<span class="visually-hidden fieldset-legend">address_basic</span>');
+
+    // Check advanced fieldset, legend, help, and description.
+    $this->assertRaw('<fieldset data-drupal-selector="edit-address-advanced" aria-describedby="edit-address-advanced--wrapper--description" id="edit-address-advanced--wrapper" class="address--wrapper fieldgroup form-composite webform-composite-visible-title js-webform-type-address webform-type-address js-form-item form-item js-form-wrapper form-wrapper">');
+    $this->assertRaw('<span class="fieldset-legend">address_advanced<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;address_advanced&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;This is help text&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
+    $this->assertRaw('<div class="description"><div id="edit-address-advanced--wrapper--description" class="webform-element-description">This is a description</div>');
+
+    /**************************************************************************/
+    // Processing.
+    /**************************************************************************/
+
+    // Check submitted value.
+    $sid = $this->postSubmission($webform);
+    $this->assertRaw("address:
+  country_code: US
+  langcode: en
+  given_name: John
+  family_name: Smith
+  organization: 'Google Inc.'
+  address_line1: '1098 Alta Ave'
+  address_line2: ''
+  locality: 'Mountain View'
+  administrative_area: CA
+  postal_code: '94043'
+  additional_name: null
+  sorting_code: null
+  dependent_locality: null
+address_advanced:
+  country_code: US
+  langcode: en
+  address_line1: '1098 Alta Ave'
+  address_line2: ''
+  locality: 'Mountain View'
+  administrative_area: CA
+  postal_code: '94043'
+  given_name: null
+  additional_name: null
+  family_name: null
+  organization: null
+  sorting_code: null
+  dependent_locality: null
+address_none: null
+address_multiple:
+  - country_code: US
+    langcode: en
+    given_name: John
+    family_name: Smith
+    organization: 'Google Inc.'
+    address_line1: '1098 Alta Ave'
+    address_line2: ''
+    locality: 'Mountain View'
+    administrative_area: CA
+    postal_code: '94043'");
+
+    // Check text formatting.
+    $this->drupalGet("/admin/structure/webform/manage/test_element_address/submission/$sid/text");
+    $this->assertRaw('address_basic:
+John Smith
+Google Inc.
+1098 Alta Ave
+Mountain View, CA 94043
+United States
+
+address_advanced:
+1098 Alta Ave
+Mountain View, CA 94043
+United States
+
+address_none:
+{Empty}
+
+address_multiple:
+- John Smith
+  Google Inc.
+  1098 Alta Ave
+  Mountain View, CA 94043
+  United States');
+
+    /**************************************************************************/
+    // Schema.
+    /**************************************************************************/
+
+    $field_storage = FieldStorageConfig::create([
+      'entity_type' => 'node',
+      'field_name' => 'address',
+      'type' => 'address',
+    ]);
+    $schema = $field_storage->getSchema();
+
+    /** @var \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager */
+    $element_manager = \Drupal::service('plugin.manager.webform.element');
+    /** @var \Drupal\webform\Plugin\WebformElement\Address $element_plugin */
+    $element_plugin = $element_manager->getElementInstance(['#type' => 'address']);
+
+    // Get webform address element plugin.
+    $element = [];
+    $element_plugin->initializeCompositeElements($element);
+
+    // Check composite elements against address schema.
+    $composite_elements = $element['#webform_composite_elements'];
+    $diff_composite_elements = array_diff_key($composite_elements, $schema['columns']);
+    $this->debug($diff_composite_elements);
+    $this->assert(empty($diff_composite_elements));
+
+    // Check composite elements maxlength against address schema.
+    foreach ($schema['columns'] as $column_name => $column) {
+      $this->assertEqual($composite_elements[$column_name]['#maxlength'], $column['length']);
+    }
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementAllowsTagsTest.php b/web/modules/webform/src/Tests/Element/WebformElementAllowsTagsTest.php
index 8e3702f434185ecb74fdb31af7f35caa5088737b..ea5c752200f7b24df73a128fd4f2d75d711589bd 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementAllowsTagsTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementAllowsTagsTest.php
@@ -21,15 +21,15 @@ class WebformElementAllowsTagsTest extends WebformElementTestBase {
    */
   public function testAllowsTags() {
     // Check <b> tags is allowed.
-    $this->drupalGet('webform/test_element_allowed_tags');
-    $this->assertRaw('Hello <b>...Goodbye</b>');
+    $this->drupalGet('/webform/test_element_allowed_tags');
+    $this->assertRaw('Hello <b>…Goodbye</b>');
 
     // Check custom <ignored> <tag> is allowed and <b> tag removed.
     \Drupal::configFactory()->getEditable('webform.settings')
       ->set('element.allowed_tags', 'ignored tag')
       ->save();
-    $this->drupalGet('webform/test_element_allowed_tags');
-    $this->assertRaw('Hello <ignored></tag>...Goodbye');
+    $this->drupalGet('/webform/test_element_allowed_tags');
+    $this->assertRaw('Hello <ignored></tag>…Goodbye');
 
     // Restore admin tags.
     \Drupal::configFactory()->getEditable('webform.settings')
diff --git a/web/modules/webform/src/Tests/Element/WebformElementAutocompleteTest.php b/web/modules/webform/src/Tests/Element/WebformElementAutocompleteTest.php
index 5435012c43dbaea7954849d123b445860ad543f8..1e957d0069a3b00adb07175fd09cff1687f6f9b7 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementAutocompleteTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementAutocompleteTest.php
@@ -26,53 +26,53 @@ public function testAutocomplete() {
 
     /* Test #autocomplete property */
 
-    $this->drupalGet('webform/test_element_autocomplete');
+    $this->drupalGet('/webform/test_element_autocomplete');
     $this->assertRaw('<input autocomplete="off" data-drupal-selector="edit-autocomplete-off" type="email" id="edit-autocomplete-off" name="autocomplete_off" value="" size="60" maxlength="254" class="form-email" />');
 
     /* Test #autocomplete_items element property */
 
     // Check routes data-drupal-selector.
-    $this->drupalGet('webform/test_element_autocomplete');
+    $this->drupalGet('/webform/test_element_autocomplete');
     $this->assertRaw('<input data-drupal-selector="edit-autocomplete-items" class="form-autocomplete form-text webform-autocomplete" data-autocomplete-path="' . $base_path . 'webform/test_element_autocomplete/autocomplete/autocomplete_items" type="text" id="edit-autocomplete-items" name="autocomplete_items" value="" size="60" maxlength="255" />');
 
     // Check #autocomplete_items partial match.
-    $this->drupalGet('webform/test_element_autocomplete/autocomplete/autocomplete_items', ['query' => ['q' => 'United']]);
+    $this->drupalGet('/webform/test_element_autocomplete/autocomplete/autocomplete_items', ['query' => ['q' => 'United']]);
     $this->assertRaw('[{"value":"United Arab Emirates","label":"United Arab Emirates"},{"value":"United Kingdom","label":"United Kingdom"},{"value":"United States","label":"United States"}]');
 
     // Check #autocomplete_items exact match.
-    $this->drupalGet('webform/test_element_autocomplete/autocomplete/autocomplete_items', ['query' => ['q' => 'United States']]);
+    $this->drupalGet('/webform/test_element_autocomplete/autocomplete/autocomplete_items', ['query' => ['q' => 'United States']]);
     $this->assertRaw('[{"value":"United States","label":"United States"}]');
 
     // Check #autocomplete_items just one character.
-    $this->drupalGet('webform/test_element_autocomplete/autocomplete/autocomplete_items', ['query' => ['q' => 'U']]);
+    $this->drupalGet('/webform/test_element_autocomplete/autocomplete/autocomplete_items', ['query' => ['q' => 'U']]);
     $this->assertRaw('[{"value":"Anguilla","label":"Anguilla"},{"value":"Antigua and Barbuda","label":"Antigua and Barbuda"},{"value":"Aruba","label":"Aruba"},{"value":"Australia","label":"Australia"},{"value":"Austria","label":"Austria"}]');
 
     /* Test #autocomplete_existing element property */
 
     // Check autocomplete is not enabled until there is a submission.
-    $this->drupalGet('webform/test_element_autocomplete');
+    $this->drupalGet('/webform/test_element_autocomplete');
     $this->assertNoRaw('<input data-drupal-selector="edit-autocomplete-existing" class="form-autocomplete form-text" data-autocomplete-path="' . $base_path . 'webform/test_element_autocomplete/autocomplete/autocomplete_existing" type="text" id="edit-autocomplete-existing" name="autocomplete_existing" value="" size="60" maxlength="255" />');
     $this->assertRaw('<input data-drupal-selector="edit-autocomplete-existing" type="text" id="edit-autocomplete-existing" name="autocomplete_existing" value="" size="60" maxlength="255" class="form-text webform-autocomplete" />');
 
     // Check #autocomplete_existing no match.
-    $this->drupalGet('webform/test_element_autocomplete/autocomplete/autocomplete_existing', ['query' => ['q' => 'abc']]);
+    $this->drupalGet('/webform/test_element_autocomplete/autocomplete/autocomplete_existing', ['query' => ['q' => 'abc']]);
     $this->assertRaw('[]');
 
     // Add #autocomplete_existing values to the submission table.
     $this->drupalPostForm('webform/test_element_autocomplete', ['autocomplete_existing' => 'abcdefg'], t('Submit'));
 
     // Check #autocomplete_existing enabled now that there is submission.
-    $this->drupalGet('webform/test_element_autocomplete');
+    $this->drupalGet('/webform/test_element_autocomplete');
     $this->assertRaw('<input data-drupal-selector="edit-autocomplete-existing" class="form-autocomplete form-text webform-autocomplete" data-autocomplete-path="' . $base_path . 'webform/test_element_autocomplete/autocomplete/autocomplete_existing" type="text" id="edit-autocomplete-existing" name="autocomplete_existing" value="" size="60" maxlength="255" />');
     $this->assertNoRaw('<input data-drupal-selector="edit-autocomplete-existing" type="text" id="edit-autocomplete-existing" name="autocomplete_existing" value="" size="60" maxlength="255" class="form-text webform-autocomplete" />');
 
     // Check #autocomplete_existing match.
-    $this->drupalGet('webform/test_element_autocomplete/autocomplete/autocomplete_existing', ['query' => ['q' => 'abc']]);
+    $this->drupalGet('/webform/test_element_autocomplete/autocomplete/autocomplete_existing', ['query' => ['q' => 'abc']]);
     $this->assertNoRaw('[]');
     $this->assertRaw('[{"value":"abcdefg","label":"abcdefg"}]');
 
     // Check #autocomplete_existing minimum number of characters < 3.
-    $this->drupalGet('webform/test_element_autocomplete/autocomplete/autocomplete_existing', ['query' => ['q' => 'ab']]);
+    $this->drupalGet('/webform/test_element_autocomplete/autocomplete/autocomplete_existing', ['query' => ['q' => 'ab']]);
     $this->assertRaw('[]');
     $this->assertNoRaw('[{"value":"abcdefg","label":"abcdefg"}]');
 
@@ -82,7 +82,7 @@ public function testAutocomplete() {
     $this->drupalPostForm('webform/test_element_autocomplete', ['autocomplete_both' => 'Existing Item'], t('Submit'));
 
     // Check #autocomplete_both match.
-    $this->drupalGet('webform/test_element_autocomplete/autocomplete/autocomplete_both', ['query' => ['q' => 'Item']]);
+    $this->drupalGet('/webform/test_element_autocomplete/autocomplete/autocomplete_both', ['query' => ['q' => 'Item']]);
     $this->assertNoRaw('[]');
     $this->assertRaw('[{"value":"Example Item","label":"Example Item"},{"value":"Existing Item","label":"Existing Item"}]');
   }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementCaptchaTest.php b/web/modules/webform/src/Tests/Element/WebformElementCaptchaTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c5c48f265ebcb315748b9e5a7a47c013ad36a95e
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementCaptchaTest.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+/**
+ * Tests for CAPTCHA element.
+ *
+ * @group Webform
+ */
+class WebformElementCaptchaTest extends WebformElementTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_captcha'];
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'captcha'];
+
+  /**
+   * Test CAPTCHA element.
+   */
+  public function testCaptcha() {
+    $this->drupalGet('/webform/test_element_captcha');
+
+    // Check default title and description.
+    $this->assertRaw('<label for="edit-captcha-response" class="js-form-required form-required">Math question</label>');
+    $this->assertRaw('Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.');
+
+    // Check CAPTCHA element custom title and description.
+    $this->assertRaw('<label for="edit-captcha-response--4" class="js-form-required form-required">{captcha_math_title}</label>');
+    $this->assertRaw('{captcha_math_description}');
+
+    // Enable CAPTCHA admin mode.
+    \Drupal::configFactory()
+      ->getEditable('captcha.settings')
+      ->set('administration_mode', TRUE)
+      ->save();
+
+    // Login root user.
+    $this->drupalLogin($this->rootUser);
+
+    // Check add CAPTCHA element text.
+    $this->drupalGet('/webform/contact');
+    $this->assertRaw('CAPTCHA should be added as an element to this webform.');
+
+    // Check replace CAPTCHA element text.
+    $this->drupalGet('/webform/test_element_captcha');
+    $this->assertNoRaw('/admin/structure/webform/manage/test_element_captcha/element/captcha/edit');
+    $this->assertRaw('Untrusted users will see a CAPTCHA element on this webform.');
+
+    // Install the Webform UI.
+    \Drupal::service('module_installer')->install(['webform_ui']);
+
+    // Check add CAPTCHA element text.
+    $this->drupalGet('/webform/contact');
+    $this->assertRaw('Add CAPTCHA element to this webform for untrusted users.');
+
+    // Check replace CAPTCHA element text.
+    $this->drupalGet('/webform/test_element_captcha');
+    $this->assertRaw('/admin/structure/webform/manage/test_element_captcha/element/captcha/edit');
+    $this->assertRaw('Untrusted users will see a CAPTCHA element on this webform.');
+
+    // Disable replace CAPTCHA admin mode.
+    \Drupal::configFactory()
+      ->getEditable('webform.settings')
+      ->set('third_party_settings.captcha.replace_administration_mode', FALSE)
+      ->save();
+
+    // Check add CAPTCHA not replaced.
+    $this->drupalGet('/webform/contact');
+    $this->assertNoRaw('Add CAPTCHA element to this webform for untrusted users.');
+    $this->assertRaw('Place a CAPTCHA here for untrusted users.');
+
+    // Enabled replace CAPTCHA admin mode and exclude the CAPTCHA element.
+    \Drupal::configFactory()
+      ->getEditable('webform.settings')
+      ->set('element.excluded_elements', ['captcha' => 'captcha'])
+      ->set('third_party_settings.captcha.replace_administration_mode', FALSE)
+      ->save();
+
+    // Check add CAPTCHA is still not replaced.
+    $this->drupalGet('/webform/contact');
+    $this->assertNoRaw('Add CAPTCHA element to this webform for untrusted users.');
+    $this->assertRaw('Place a CAPTCHA here for untrusted users.');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementCheckboxesTest.php b/web/modules/webform/src/Tests/Element/WebformElementCheckboxesTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..2cec2d3c745409179059df6fea2c14b67e077be6
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementCheckboxesTest.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+use Drupal\webform\Entity\Webform;
+
+/**
+ * Tests for webform checkboxes element.
+ *
+ * @group Webform
+ */
+class WebformElementCheckboxesTest extends WebformElementTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_checkboxes'];
+
+  /**
+   * Tests checkbox and checkboxes element.
+   */
+  public function testCheckboxes() {
+    $webform = Webform::load('test_element_checkboxes');
+
+    // Check exclude empty is not visible.
+    $edit = [
+      'checkboxes_required_conditions[Yes]' => TRUE,
+      'checkboxes_other_required_conditions[checkboxes][Yes]' => TRUE,
+    ];
+    $this->postSubmission($webform, $edit, t('Preview'));
+    $this->assertNoRaw('<label>checkbox_exclude_empty</label>');
+
+    // Uncheck #exclude_empty.
+    $webform->setElementProperties('checkbox_exclude_empty', ['#type' => 'checkbox', '#title' => 'checkbox_exclude_empty']);
+    $webform->save();
+
+    // Check exclude empty is visible.
+    $edit = [
+      'checkboxes_required_conditions[Yes]' => TRUE,
+      'checkboxes_other_required_conditions[checkboxes][Yes]' => TRUE,
+    ];
+    $this->postSubmission($webform, $edit, t('Preview'));
+    $this->assertRaw('<label>checkbox_exclude_empty</label>');
+
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementCodeMirrorTest.php b/web/modules/webform/src/Tests/Element/WebformElementCodeMirrorTest.php
index 862e68964f094cb1efe7738c3972f86442358ff5..38108b36d94cafe1f87d6d9b3c25bfe63c68cccb 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementCodeMirrorTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementCodeMirrorTest.php
@@ -26,16 +26,21 @@ public function testCodeMirror() {
     /**************************************************************************/
 
     // Check Text.
-    $this->drupalGet('webform/test_element_codemirror');
+    $this->drupalGet('/webform/test_element_codemirror');
     $this->assertRaw('<label for="edit-text-basic">text_basic</label>');
     $this->assertRaw('<textarea data-drupal-selector="edit-text-basic" class="js-webform-codemirror webform-codemirror text form-textarea resize-vertical" data-webform-codemirror-mode="text/plain" id="edit-text-basic" name="text_basic" rows="5" cols="60">Hello</textarea>');
 
+    // Check Text with no wrap.
+    $this->drupalGet('/webform/test_element_codemirror');
+    $this->assertRaw('<label for="edit-text-basic-no-wrap">text_basic_no_wrap</label>');
+    $this->assertRaw('<textarea data-drupal-selector="edit-text-basic-no-wrap" wrap="off" class="js-webform-codemirror webform-codemirror text form-textarea resize-vertical" data-webform-codemirror-mode="text/plain" id="edit-text-basic-no-wrap" name="text_basic_no_wrap" rows="5" cols="60">');
+
     /**************************************************************************/
     // code:yaml
     /**************************************************************************/
 
     // Check YAML.
-    $this->drupalGet('webform/test_element_codemirror');
+    $this->drupalGet('/webform/test_element_codemirror');
     $this->assertRaw('<label for="edit-yaml-basic">yaml_basic</label>');
     $this->assertRaw('<textarea data-drupal-selector="edit-yaml-basic" class="js-webform-codemirror webform-codemirror yaml form-textarea resize-vertical" data-webform-codemirror-mode="text/x-yaml" id="edit-yaml-basic" name="yaml_basic" rows="5" cols="60">test: hello</textarea>');
 
@@ -65,7 +70,7 @@ public function testCodeMirror() {
     /**************************************************************************/
 
     // Check HTML.
-    $this->drupalGet('webform/test_element_codemirror');
+    $this->drupalGet('/webform/test_element_codemirror');
     $this->assertRaw('<label for="edit-html-basic">html_basic</label>');
     $this->assertRaw('<textarea data-drupal-selector="edit-html-basic" class="js-webform-codemirror webform-codemirror html form-textarea resize-vertical" data-webform-codemirror-mode="text/html" id="edit-html-basic" name="html_basic" rows="5" cols="60">&lt;b&gt;Hello&lt;/b&gt;</textarea>');
 
@@ -90,7 +95,7 @@ public function testCodeMirror() {
     /**************************************************************************/
 
     // Check disabled Twig editor.
-    $this->drupalGet('webform/test_element_codemirror');
+    $this->drupalGet('/webform/test_element_codemirror');
     $this->assertRaw('<label for="edit-twig-basic">twig_basic</label>');
     $this->assertRaw('<textarea data-drupal-selector="edit-twig-basic" disabled="disabled" class="js-webform-codemirror webform-codemirror twig form-textarea resize-vertical" data-webform-codemirror-mode="twig" id="edit-twig-basic" name="twig_basic" rows="5" cols="60">
 {% set value = &quot;Hello&quot; %}
@@ -101,7 +106,7 @@ public function testCodeMirror() {
     $this->drupalLogin($this->rootUser);
 
     // Check enabled Twig editor.
-    $this->drupalGet('webform/test_element_codemirror');
+    $this->drupalGet('/webform/test_element_codemirror');
     $this->assertRaw('<textarea data-drupal-selector="edit-twig-basic" class="js-webform-codemirror webform-codemirror twig form-textarea resize-vertical" data-webform-codemirror-mode="twig" id="edit-twig-basic" name="twig_basic" rows="5" cols="60">
 {% set value = &quot;Hello&quot; %}
 {{ value }}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementCompositeTest.php b/web/modules/webform/src/Tests/Element/WebformElementCompositeTest.php
index 44909d6a2efb78a18977c851cf719ddf9573f6d2..130367d20a75712c6ad87776465f7f5e16126de3 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementCompositeTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementCompositeTest.php
@@ -16,12 +16,20 @@ class WebformElementCompositeTest extends WebformElementTestBase {
    *
    * @var array
    */
-  protected static $testWebforms = ['test_element_composite'];
+  protected static $testWebforms = [
+    'test_element_composite',
+    'test_element_composite_wrapper',
+  ];
 
   /**
    * Test composite (builder).
    */
   public function testComposite() {
+
+    /**************************************************************************/
+    // Builder.
+    /**************************************************************************/
+
     $webform = Webform::load('test_element_composite');
 
     // Check processing for user who can't edit source.
@@ -29,12 +37,12 @@ public function testComposite() {
     $this->assertRaw("webform_element_composite_basic:
   first_name:
     '#type': textfield
-    '#title': 'First name'
     '#required': true
+    '#title': 'First name'
   last_name:
     '#type': textfield
-    '#title': 'Last name'
     '#required': true
+    '#title': 'Last name'
 webform_element_composite_advanced:
   first_name:
     '#type': textfield
@@ -69,12 +77,12 @@ public function testComposite() {
     $this->assertRaw("webform_element_composite_basic:
   first_name:
     '#type': textfield
-    '#title': 'First name'
     '#required': true
+    '#title': 'First name'
   last_name:
     '#type': textfield
-    '#title': 'Last name'
     '#required': true
+    '#title': 'Last name'
 webform_element_composite_advanced:
   first_name:
     '#type': textfield
@@ -102,6 +110,38 @@ public function testComposite() {
     '#field_suffix': ' yrs. old'
     '#min': 1
     '#max': 125");
+
+    /**************************************************************************/
+    // Wrapper.
+    /**************************************************************************/
+
+    $this->drupalGet('/webform/test_element_composite_wrapper');
+
+    // Check fieldset wrapper.
+    $this->assertRaw('<fieldset data-drupal-selector="edit-radios-wrapper-fieldset" id="edit-radios-wrapper-fieldset--wrapper" class="radios--wrapper fieldgroup form-composite webform-composite-visible-title required js-webform-type-radios webform-type-radios js-form-item form-item js-form-wrapper form-wrapper">');
+
+    // Check fieldset wrapper with hidden title.
+    $this->assertRaw('<fieldset data-drupal-selector="edit-radios-wrapper-fieldset-hidden-title" id="edit-radios-wrapper-fieldset-hidden-title--wrapper" class="radios--wrapper fieldgroup form-composite webform-composite-hidden-title required js-webform-type-radios webform-type-radios js-form-item form-item js-form-wrapper form-wrapper">');
+
+    // Check form element wrapper.
+    $this->assertRaw('<div class="js-form-item form-item js-form-type-radios form-type-radios js-form-item-radios-wrapper-form-element form-item-radios-wrapper-form-element">');
+
+    // Check container wrapper.
+    $this->assertRaw('<div data-drupal-selector="edit-radios-wrapper-container" id="edit-radios-wrapper-container--wrapper" class="radios--wrapper fieldgroup form-composite js-form-wrapper form-wrapper">');
+
+    // Check radios 'aria-describedby' with wrapper description.
+    $this->assertRaw('<input data-drupal-selector="edit-radios-wrapper-fieldset-description-one" aria-describedby="edit-radios-wrapper-fieldset-description--wrapper--description" type="radio" id="edit-radios-wrapper-fieldset-description-one" name="radios_wrapper_fieldset_description" value="One" class="form-radio" />');
+    $this->assertRaw('<div class="description"><div id="edit-radios-wrapper-fieldset-description--wrapper--description" class="webform-element-description">This is a description</div>');
+
+    // Check wrapper with #states.
+    $this->assertRaw('<fieldset data-drupal-selector="edit-states-fieldset" id="edit-states-fieldset--wrapper" class="radios--wrapper fieldgroup form-composite webform-composite-visible-title js-webform-type-radios webform-type-radios js-form-item form-item js-form-wrapper form-wrapper" data-drupal-states="{&quot;visible&quot;:{&quot;.webform-submission-test-element-composite-wrapper-add-form :input[name=\u0022states_checkbox\u0022]&quot;:{&quot;checked&quot;:true}}}">');
+    $this->assertRaw('<div data-drupal-states="{&quot;visible&quot;:{&quot;.webform-submission-test-element-composite-wrapper-add-form :input[name=\u0022states_checkbox\u0022]&quot;:{&quot;checked&quot;:true}}}" class="js-form-item form-item js-form-type-radios form-type-radios js-form-item-states-form-item form-item-states-form-item">');
+    $this->assertRaw('<div data-drupal-selector="edit-states-container" id="edit-states-container--wrapper" class="radios--wrapper fieldgroup form-composite js-form-wrapper form-wrapper" data-drupal-states="{&quot;visible&quot;:{&quot;.webform-submission-test-element-composite-wrapper-add-form :input[name=\u0022states_checkbox\u0022]&quot;:{&quot;checked&quot;:true}}}">');
+
+    // Below tests are only failing on Drupal.org and pass locally.
+    // Check radios 'aria-describedby' with individual descriptions.
+    // $this->assertRaw('<input data-drupal-selector="edit-radios-wrapper-fieldset-element-descriptions-one" aria-describedby="edit-radios-wrapper-fieldset-element-descriptions-one--description" type="radio" id="edit-radios-wrapper-fieldset-element-descriptions-one" name="radios_wrapper_fieldset_element_descriptions" value="One" class="form-radio" />');
+    // $this->assertRaw('<div id="edit-radios-wrapper-fieldset-element-descriptions-one--description" class="webform-element-description">This is a radio description</div>');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementComputedTest.php b/web/modules/webform/src/Tests/Element/WebformElementComputedTest.php
index 36ccb88fd7d21e55f9e5802be4e1c02e96f39f4e..62fce7514d4868f5b495ea25fec2ead12eb83c87 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementComputedTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementComputedTest.php
@@ -24,7 +24,11 @@ class WebformElementComputedTest extends WebformElementTestBase {
    *
    * @var array
    */
-  protected static $testWebforms = ['test_element_computed_token', 'test_element_computed_twig'];
+  protected static $testWebforms = [
+    'test_element_computed_token',
+    'test_element_computed_twig',
+    'test_element_computed_ajax',
+  ];
 
   /**
    * {@inheritdoc}
@@ -64,7 +68,7 @@ public function testComputedElement() {
     $this->assertRaw('<b class="webform_computed_token_html">xss:</b> &lt;script&gt;alert(&quot;XSS&quot;);&lt;/script&gt;<br />');
 
     // Check token plain text rendering.
-    $this->assertRaw('<div id="test_element_computed_token--webform_computed_token_text" class="webform-element webform-element-type-webform-computed-token js-form-item form-item js-form-type-item form-type-item js-form-item-webform-computed-token-text form-item-webform-computed-token-text">');
+    $this->assertRaw('<div class="webform-element webform-element-type-webform-computed-token js-form-item form-item js-form-type-item form-type-item js-form-item-webform-computed-token-text form-item-webform-computed-token-text" id="test_element_computed_token--webform_computed_token_text">');
     $this->assertRaw('<label>webform_computed_token_text</label>');
     $this->assertRaw('simple string: This is a string<br />');
     $this->assertRaw('complex string : This is a &lt;strong&gt;complex&lt;/strong&gt; string, which contains &quot;double&quot; and &#039;single&#039; quotes with special characters like &gt;, &lt;, &gt;&lt;, and &lt;&gt;.<br />');
@@ -76,7 +80,8 @@ public function testComputedElement() {
     $data = $webform_submission->getData();
 
     // Check value stored in the database.
-    $this->assertEqual($data['webform_computed_token_store'], 'This is a string');
+    $this->debug($data['webform_computed_token_store']);
+    $this->assertEqual($data['webform_computed_token_store'], "sid: $sid");
 
     // Check values not stored in the database.
     $this->assert(!isset($data['webform_computed_token_auto']));
@@ -85,6 +90,15 @@ public function testComputedElement() {
 
     /* Twig */
 
+    // Get computed Twig form.
+    $this->drupalGet('/webform/test_element_computed_twig');
+
+    // Check Twig trim.
+    $this->assertFieldByName('webform_computed_twig_trim', '<em>This is trimmed</em>  <br/>');
+
+    // Check Twig spaceless.
+    $this->assertFieldByName('webform_computed_twig_spaceless', '<em>This is spaceless</em><br/>');
+
     // Get computed Twig preview.
     $this->drupalPostForm('webform/test_element_computed_twig', [], t('Preview'));
 
@@ -114,6 +128,54 @@ public function testComputedElement() {
     $this->assertRaw('<b class="webform_computed_twig_data">complex string:</b> This is a &lt;strong&gt;complex&lt;/strong&gt; string, which contains &quot;double&quot; and &#039;single&#039; quotes with special characters like &gt;, &lt;, &gt;&lt;, and &lt;&gt;.<br />');
     $this->assertRaw('<b class="webform_computed_twig_data">text_format:</b> &lt;p&gt;This is a &lt;strong&gt;text format&lt;/strong&gt; string.&lt;/p&gt;');
     $this->assertRaw('<b class="webform_computed_twig_data">xss:</b> &lt;script&gt;alert(&quot;XSS&quot;);&lt;/script&gt;<br />');
+
+    /* Ajax */
+
+    // Get computed ajax form.
+    $this->drupalGet('/webform/test_element_computed_ajax');
+
+    // Check that a and b are hidden via #hide_empty.
+    $this->assertRaw('<div style="display:none" class="js-form-item form-item js-form-type-item form-type-item js-form-item-webform-computed-token-a form-item-webform-computed-token-a">');
+    $this->assertRaw('<div style="display:none" class="js-form-item form-item js-form-type-item form-type-item js-form-item-webform-computed-token-b form-item-webform-computed-token-b">');
+
+    // Check a, b, computed default values.
+    $this->assertFieldByName('webform_computed_token_a', '');
+    $this->assertFieldByName('webform_computed_token_b', '');
+    $this->assertFieldByName('webform_computed_twig', 'Please enter a value for a and b.');
+    $this->assertFieldByName('webform_computed_twig_token', 'Please enter a value for a and b.');
+
+    // Calculate 2 + 4 = 6.
+    $edit = ['a[select]' => 2, 'b' => 4];
+
+    // Check a is updated.
+    $this->drupalPostAjaxForm(NULL, $edit, 'webform-computed-webform_computed_token_a-button');
+    $this->assertNoRaw('<div style="display:none" class="js-form-item form-item js-form-type-item form-type-item js-form-item-webform-computed-token-a form-item-webform-computed-token-a">');
+    $this->assertFieldByName('webform_computed_token_a', '2');
+    $this->assertFieldByName('webform_computed_token_b', '');
+    $this->assertFieldByName('webform_computed_twig', 'Please enter a value for a and b.');
+    $this->assertFieldByName('webform_computed_twig_token', 'Please enter a value for a and b.');
+
+    // Check b is updated.
+    $this->drupalPostAjaxForm(NULL, $edit, 'webform-computed-webform_computed_token_b-button');
+    $this->assertNoRaw('<div style="display:none" class="js-form-item form-item js-form-type-item form-type-item js-form-item-webform-computed-token-b form-item-webform-computed-token-b">');
+    $this->assertFieldByName('webform_computed_token_a', '2');
+    $this->assertFieldByName('webform_computed_token_b', '4');
+    $this->assertFieldByName('webform_computed_twig', 'Please enter a value for a and b.');
+    $this->assertFieldByName('webform_computed_twig_token', 'Please enter a value for a and b.');
+
+    // Check twig is updated.
+    $this->drupalPostAjaxForm(NULL, $edit, 'webform-computed-webform_computed_twig-button');
+    $this->assertFieldByName('webform_computed_token_a', '2');
+    $this->assertFieldByName('webform_computed_token_b', '4');
+    $this->assertFieldByName('webform_computed_twig', '2 + 4 = 6');
+    $this->assertFieldByName('webform_computed_twig_token', 'Please enter a value for a and b.');
+
+    // Check twig with token is updated.
+    $this->drupalPostAjaxForm(NULL, $edit, 'webform-computed-webform_computed_twig_token-button');
+    $this->assertFieldByName('webform_computed_token_a', '2');
+    $this->assertFieldByName('webform_computed_token_b', '4');
+    $this->assertFieldByName('webform_computed_twig', '2 + 4 = 6');
+    $this->assertFieldByName('webform_computed_twig_token', '2 + 4 = 6');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementCounterTest.php b/web/modules/webform/src/Tests/Element/WebformElementCounterTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..bd0a0aec057e1e48ba0d9a6f28ff35ecfe72fe6e
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementCounterTest.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+/**
+ * Tests for webform (text) counter.
+ *
+ * @group Webform
+ */
+class WebformElementCounterTest extends WebformElementTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_counter'];
+
+  /**
+   * Tests text elements.
+   */
+  public function testCounter() {
+
+    // Check counters.
+    $this->drupalGet('/webform/test_element_counter');
+    $this->assertRaw('<input data-counter-type="character" data-counter-minimum="5" data-counter-minimum-message="%d character(s) entered. This is custom text" class="js-webform-counter webform-counter form-text" data-drupal-selector="edit-counter-characters-min-message" type="text" id="edit-counter-characters-min-message" name="counter_characters_min_message" value="" size="60" maxlength="255" />');
+    $this->assertRaw('<input data-counter-type="character" data-counter-maximum="10" data-counter-maximum-message="%d character(s) remaining. This is custom text" class="js-webform-counter webform-counter form-text" data-drupal-selector="edit-counter-characters-max-message" type="text" id="edit-counter-characters-max-message" name="counter_characters_max_message" value="" size="60" maxlength="10" />');
+    $this->assertRaw('<textarea data-counter-type="word" data-counter-minimum="5" data-counter-minimum-message="%d word(s) entered. This is custom text" class="js-webform-counter webform-counter form-textarea resize-vertical" data-drupal-selector="edit-counter-words-min-message" id="edit-counter-words-min-message" name="counter_words_min_message" rows="5" cols="60"></textarea>');
+    $this->assertRaw('<textarea data-counter-type="word" data-counter-maximum="10" data-counter-maximum-message="%d character(s) remaining. This is custom text" class="js-webform-counter webform-counter form-textarea resize-vertical" data-drupal-selector="edit-counter-words-max-message" id="edit-counter-words-max-message" name="counter_words_max_message" rows="5" cols="60"></textarea>');
+
+    // Check counter min/max validation error (min: 5 / max: 10).
+    $edit = [
+      'counter_characters_min' => '123',
+      'counter_characters_max' => '1234567890xxx',
+      'counter_words_min' => 'one two three',
+      'counter_words_max' => 'one two three four five six seven eight nine ten eleven',
+    ];
+    $this->drupalPostForm('webform/test_element_counter', $edit, t('Submit'));
+    $this->assertRaw('counter_characters_min (5) must be longer than <em class="placeholder">5</em> characters but is currently <em class="placeholder">3</em> characters long.');
+    $this->assertRaw('counter_characters_max (10) cannot be longer than <em class="placeholder">10</em> characters but is currently <em class="placeholder">13</em> characters long.');
+    $this->assertRaw('counter_words_min (5) must be longer than <em class="placeholder">5</em> words but is currently <em class="placeholder">3</em> words long.');
+    $this->assertRaw('counter_words_max (10) cannot be longer than <em class="placeholder">10</em> words but is currently <em class="placeholder">11</em> words long.');
+
+    // Check counter validation passes (min: 5 / max: 10).
+    $edit = [
+      'counter_characters_min' => '12345',
+      'counter_characters_max' => '1234567890',
+      'counter_words_min' => 'one two three four five',
+      'counter_words_max' => 'one two three four five six seven eight nine ten',
+    ];
+    $this->drupalPostForm('webform/test_element_counter', $edit, t('Submit'));
+    $this->assertNoRaw('counter_characters_min (5) must be longer than <em class="placeholder">5</em> characters');
+    $this->assertNoRaw('counter_characters_max (10) cannot be longer than <em class="placeholder">10</em> characters');
+    $this->assertNoRaw('counter_words_min (5) must be longer than <em class="placeholder">5</em> words');
+    $this->assertNoRaw('counter_words_max (10) cannot be longer than <em class="placeholder">10</em> words');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementCustomPropertiesTest.php b/web/modules/webform/src/Tests/Element/WebformElementCustomPropertiesTest.php
index c9098976fbc3d045aacb7e6a96b0bb0aa906269b..df98980866156b83f603b11f7cb2eca349f4c1bc 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementCustomPropertiesTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementCustomPropertiesTest.php
@@ -36,7 +36,7 @@ public function testCustomProperties() {
     $name_element = [
       '#type' => 'textfield',
       '#title' => 'Your Name',
-      '#default_value' => '[webform-authenticated-user:display-name]',
+      '#default_value' => '[current-user:display-name]',
       '#required' => TRUE,
     ];
 
@@ -45,7 +45,7 @@ public function testCustomProperties() {
     $this->assertEqual($webform->getElementDecoded('name'), $name_element);
 
     // Check that name input does not contain custom data.
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertRaw('<input data-drupal-selector="edit-name" type="text" id="edit-name" name="name" value="' . htmlentities($admin_user->label()) . '" size="60" maxlength="255" class="form-text required" required="required" aria-required="true" />');
 
     // Submit empty custom property and data.
@@ -79,7 +79,7 @@ public function testCustomProperties() {
     $this->assertEqual($webform->getElementDecoded('name'), $name_element);
 
     // Check that name input does contain custom data.
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertRaw('<input data-custom="custom-data" data-drupal-selector="edit-name" type="text" id="edit-name" name="name" value="' . htmlentities($admin_user->label()) . '" size="60" maxlength="255" class="form-text required" required="required" aria-required="true" />');
   }
 
diff --git a/web/modules/webform/src/Tests/Element/WebformElementDateListTest.php b/web/modules/webform/src/Tests/Element/WebformElementDateListTest.php
index 29bd1c3f39de6e573c60732d5ac3dd5e1f243c77..b398662d8da884fcecf8f3c3c199000054fb3956 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementDateListTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementDateListTest.php
@@ -25,19 +25,42 @@ class WebformElementDateListTest extends WebformElementTestBase {
   public function testDateListElement() {
     $webform = Webform::load('test_element_datelist');
 
+    // Check posted submission values.
+    $this->postSubmission($webform);
+    $this->assertRaw("datelist_default: '2009-08-18T16:00:00+1000'
+datelist_no_abbreviate: '2009-08-18T16:00:00+1000'
+datelist_text_parts: '2009-08-18T16:00:00+1000'
+datelist_datetime: '2009-08-18T16:00:00+1000'
+datelist_date: '2009-08-18T00:00:00+1000'
+datelist_min_max: '2009-08-18T00:00:00+1000'
+datelist_min_max_time: '2009-01-01T09:00:00+1100'
+datelist_date_year_range_reverse: ''
+datelist_required_error: '2009-08-18T16:00:00+1000'
+datelist_multiple:
+  - '2009-08-18T16:00:00+1000'
+datelist_custom_composite:
+  - datelist: '2009-08-18T16:00:00+1000'");
+
+    $this->drupalGet('/webform/test_element_datelist');
+
+    // Check datelist label has not for attributes.
+    $this->assertRaw('<label>datelist_default</label>');
+
     // Check '#format' values.
-    $this->drupalGet('webform/test_element_datelist');
     $this->assertFieldByName('datelist_default[month]', '8');
 
+    // Check '#date_abbreviate': false.
+    $this->assertRaw('<select data-drupal-selector="edit-datelist-no-abbreviate-month" title="Month" id="edit-datelist-no-abbreviate-month" name="datelist_no_abbreviate[month]" class="form-select"><option value="">Month</option><option value="1">January</option>');
+
     // Check date year range reverse.
-    $this->drupalGet('webform/test_element_datelist');
+    $this->drupalGet('/webform/test_element_datelist');
     $this->assertRaw('<select data-drupal-selector="edit-datelist-date-year-range-reverse-year" title="Year" id="edit-datelist-date-year-range-reverse-year" name="datelist_date_year_range_reverse[year]" class="form-select"><option value="" selected="selected">Year</option><option value="2010">2010</option><option value="2009">2009</option><option value="2008">2008</option><option value="2007">2007</option><option value="2006">2006</option><option value="2005">2005</option></select>');
 
     // Check 'datelist' and 'datetime' #default_value.
     $form = $webform->getSubmissionForm();
     $this->assert($form['elements']['datelist_default']['#default_value'] instanceof DrupalDateTime, 'datelist_default #default_value instance of \Drupal\Core\Datetime\DrupalDateTime.');
 
-    // Check datelist #max validation.
+    // Check datelist #date_date_max validation.
     $edit = [
       'datelist_min_max[year]' => '2010',
       'datelist_min_max[month]' => '8',
@@ -46,7 +69,7 @@ public function testDateListElement() {
     $this->drupalPostForm('webform/test_element_datelist', $edit, t('Submit'));
     $this->assertRaw('<em class="placeholder">datelist_min_max</em> must be on or before <em class="placeholder">2009-12-31</em>.');
 
-    // Check datelist #min validation.
+    // Check datelist #date_date_min validation.
     $edit = [
       'datelist_min_max[year]' => '2006',
       'datelist_min_max[month]' => '8',
@@ -55,6 +78,26 @@ public function testDateListElement() {
     $this->drupalPostForm('webform/test_element_datelist', $edit, t('Submit'));
     $this->assertRaw('<em class="placeholder">datelist_min_max</em> must be on or after <em class="placeholder">2009-01-01</em>.');
 
+    // Check datelist #date_max validation.
+    $edit = [
+      'datelist_min_max_time[year]' => '2009',
+      'datelist_min_max_time[month]' => '12',
+      'datelist_min_max_time[day]' => '31',
+      'datelist_min_max_time[hour]' => '18',
+    ];
+    $this->drupalPostForm('webform/test_element_datelist', $edit, t('Submit'));
+    $this->assertRaw('<em class="placeholder">datelist_min_max_time</em> must be on or before <em class="placeholder">2009-12-31 17:00:00</em>.');
+
+    // Check datelist #date_min validation.
+    $edit = [
+      'datelist_min_max_time[year]' => '2009',
+      'datelist_min_max_time[month]' => '1',
+      'datelist_min_max_time[day]' => '1',
+      'datelist_min_max_time[hour]' => '8',
+    ];
+    $this->drupalPostForm('webform/test_element_datelist', $edit, t('Submit'));
+    $this->assertRaw('<em class="placeholder">datelist_min_max_time</em> must be on or after <em class="placeholder">2009-01-01 09:00:00</em>.');
+
     // Check custom required error.
     $edit = [
       'datelist_required_error[year]' => '',
diff --git a/web/modules/webform/src/Tests/Element/WebformElementDateTest.php b/web/modules/webform/src/Tests/Element/WebformElementDateTest.php
index 77524757b53522b6ff3f52739354105434a8d9a2..aabdc4518788c8c5c4c9585a0b3d4539746b82c9 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementDateTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementDateTest.php
@@ -28,7 +28,7 @@ public function testDateElement() {
     // Render date elements.
     /**************************************************************************/
 
-    $this->drupalGet('webform/test_element_date');
+    $this->drupalGet('/webform/test_element_date');
 
     // Check '#format' values.
     $this->assertFieldByName('date_default', '2009-08-18');
@@ -56,7 +56,7 @@ public function testDateElement() {
     $this->assertRaw('<em class="placeholder">date_min_max</em> must be on or after <em class="placeholder">2009-01-01</em>.');
 
     // Check dynamic date.
-    $this->drupalGet('webform/test_element_date');
+    $this->drupalGet('/webform/test_element_date');
     $min = \Drupal::service('date.formatter')->format(strtotime('-1 year'), 'html_date');
     $min_year = date('Y', strtotime('-1 year'));
     $max = \Drupal::service('date.formatter')->format(strtotime('+1 year'), 'html_date');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementDateTimeTest.php b/web/modules/webform/src/Tests/Element/WebformElementDateTimeTest.php
index fa980de3176aa4f1bec8afb74c94262adab017cb..d8cf2e82e9dc1fb8316d9bc9cdd959f8b9d0dc3c 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementDateTimeTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementDateTimeTest.php
@@ -25,7 +25,19 @@ class WebformElementDateTimeTest extends WebformElementTestBase {
    */
   public function testDateTime() {
     $webform = Webform::load('test_element_datetime');
-    $this->drupalGet('webform/test_element_datetime');
+
+    // Check posted submission values.
+    $this->postSubmission($webform);
+    $this->assertRaw("datetime_default: '2009-08-18T16:00:00+1000'");
+    $this->assertRaw("datetime_multiple:
+  - '2009-08-18T16:00:00+1000'");
+    $this->assertRaw("datetime_custom_composite:
+  - datetime: '2009-08-18T16:00:00+1000'");
+
+    $this->drupalGet('/webform/test_element_datetime');
+
+    // Check datetime label has not for attributes.
+    $this->assertRaw('<label>datetime_default</label>');
 
     // Check '#format' values.
     $this->assertFieldByName('datetime_default[date]', '2009-08-18');
@@ -36,28 +48,50 @@ public function testDateTime() {
     $this->assertRaw('<input data-drupal-selector="edit-datetime-datepicker-timepicker-date" title="Date (e.g. ' . $now_date . ')" type="text" min="Mon, 01/01/1900" max="Sat, 12/31/2050" data-drupal-date-format="D, m/d/Y" id="edit-datetime-datepicker-timepicker-date" name="datetime_datepicker_timepicker[date]" value="Tue, 08/18/2009" size="15" maxlength="128" class="form-text" />');
     $this->assertRaw('<input data-drupal-selector="edit-datetime-datepicker-timepicker-time"');
     // Skip time which can change during the tests.
-    $this->assertRaw('type="text" step="1" data-webform-time-format="g:i A" id="edit-datetime-datepicker-timepicker-time" name="datetime_datepicker_timepicker[time]" value="4:00 PM" size="12" maxlength="128" class="form-text" />');
+    $this->assertRaw('type="text" step="1" data-webform-time-format="g:i A" id="edit-datetime-datepicker-timepicker-time" name="datetime_datepicker_timepicker[time]" value="4:00 PM" size="12" maxlength="12" class="form-time webform-time" />');
 
     // Check time with custom min/max/step attributes.
     $this->assertRaw('<input min="2009-01-01" data-min-year="2009" max="2009-12-31" data-max-year="2009" data-drupal-selector="edit-datetime-time-min-max-date"');
-    $this->assertRaw('<input min="09:00:00" data-min-year="2009" max="17:00:00" data-max-year="2009" data-drupal-selector="edit-datetime-time-min-max-time"');
+    $this->assertRaw('<input min="09:00:00" max="17:00:00" data-drupal-selector="edit-datetime-time-min-max-time"');
     $this->assertRaw('<input min="Thu, 01/01/2009" data-min-year="2009" max="Thu, 12/31/2009" data-max-year="2009" data-drupal-selector="edit-datetime-datepicker-timepicker-time-min-max-date"');
-    $this->assertRaw('<input min="09:00:00" data-min-year="2009" max="17:00:00" data-max-year="2009" data-drupal-selector="edit-datetime-datepicker-timepicker-time-min-max-time"');
+    $this->assertRaw('<input min="09:00:00" max="17:00:00" data-drupal-selector="edit-datetime-datepicker-timepicker-time-min-max-time"');
 
     // Check 'datelist' and 'datetime' #default_value.
     $form = $webform->getSubmissionForm();
     $this->assert($form['elements']['datetime_default']['#default_value'] instanceof DrupalDateTime, 'datetime_default #default_value instance of \Drupal\Core\Datetime\DrupalDateTime.');
 
-    // Check datetime #max validation.
+    // Check datetime #date_date_max validation.
     $edit = ['datetime_min_max[date]' => '2010-08-18'];
     $this->drupalPostForm('webform/test_element_datetime', $edit, t('Submit'));
     $this->assertRaw('<em class="placeholder">datetime_min_max</em> must be on or before <em class="placeholder">2009-12-31</em>.');
 
-    // Check datetime #min validation.
+    // Check datetime #date_date_min validation.
     $edit = ['datetime_min_max[date]' => '2006-08-18'];
     $this->drupalPostForm('webform/test_element_datetime', $edit, t('Submit'));
     $this->assertRaw('<em class="placeholder">datetime_min_max</em> must be on or after <em class="placeholder">2009-01-01</em>.');
 
+    // Check datetime #date_max date validation.
+    $edit = ['datetime_min_max_time[date]' => '2009-12-31', 'datetime_min_max_time[time]' => '19:00:00'];
+    $this->drupalPostForm('webform/test_element_datetime', $edit, t('Submit'));
+    $this->assertRaw('<em class="placeholder">datetime_min_max_time</em> must be on or before <em class="placeholder">2009-12-31 17:00:00</em>.');
+
+    // Check datetime #date_min date validation.
+    $edit = ['datetime_min_max_time[date]' => '2009-01-01', 'datetime_min_max_time[time]' => '08:00:00'];
+    $this->drupalPostForm('webform/test_element_datetime', $edit, t('Submit'));
+    $this->assertRaw('<em class="placeholder">datetime_min_max_time</em> must be on or after <em class="placeholder">2009-01-01 09:00:00</em>.');
+
+    // Check datetime #date_max time validation.
+    $edit = ['datetime_min_max_time[time]' => '01:00:00'];
+    $this->drupalPostForm('webform/test_element_datetime', $edit, t('Submit'));
+    $this->assertRaw('<em class="placeholder">datetime_min_max_time: Time</em> must be on or after <em class="placeholder">09:00:00</em>.');
+    $this->assertRaw('<em class="placeholder">datetime_min_max_time</em> must be on or after <em class="placeholder">2009-01-01 09:00:00</em>.');
+
+    // Check datetime #date_min time validation.
+    $edit = ['datetime_min_max_time[time]' => '01:00:00'];
+    $this->drupalPostForm('webform/test_element_datetime', $edit, t('Submit'));
+    $this->assertRaw('<em class="placeholder">datetime_min_max_time: Time</em> must be on or after <em class="placeholder">09:00:00</em>.');
+    $this->assertRaw('<em class="placeholder">datetime_min_max_time</em> must be on or after <em class="placeholder">2009-01-01 09:00:00</em>.');
+
     // Check: Issue #2723159: Datetime form element cannot validate when using a
     // format without seconds.
     $sid = $this->postSubmission($webform);
diff --git a/web/modules/webform/src/Tests/Element/WebformElementDetailsTest.php b/web/modules/webform/src/Tests/Element/WebformElementDetailsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea4967b2eae71d3a9f51814b4e7581df48dea84e
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementDetailsTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+/**
+ * Tests for details element.
+ *
+ * @group Webform
+ */
+class WebformElementDetailsTest extends WebformElementTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_details'];
+
+  /**
+   * Test details element.
+   */
+  public function testDetails() {
+    $this->drupalGet('/webform/test_element_details');
+
+    // Check details with help, field prefix, field suffix, description,
+    // and more. Also, check that invalid 'required' and 'aria-required'
+    // attributes are removed.
+    $this->assertRaw('<details data-webform-key="details" data-drupal-selector="edit-details" aria-describedby="edit-details--description" id="edit-details" class="js-form-wrapper form-wrapper required" open="open">');
+    $this->assertRaw('<summary role="button" aria-controls="edit-details" aria-expanded="true" aria-pressed="true" class="js-form-required form-required">details<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;details&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;This is help text.&lt;/div&gt;"><span aria-hidden="true">?</span>');
+    $this->assertRaw('<div class="details-description"><div id="edit-details--description" class="webform-element-description">This is a description.</div>');
+    $this->assertRaw('<div id="edit-details--more" class="js-webform-element-more webform-element-more">');
+
+    // Check details title_display: invisible.
+    $this->assertRaw('<summary role="button" aria-controls="edit-details-title-invisible" aria-expanded="false" aria-pressed="false"><span class="visually-hidden">Details title invisible</span></summary>');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementEmailTest.php b/web/modules/webform/src/Tests/Element/WebformElementEmailTest.php
index 3ae864ea75e6ef6c5ddbc3e8e55317c4801167f5..be3b22db270c232105dd6e8c4fb3e977d6efefd9 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementEmailTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementEmailTest.php
@@ -26,7 +26,7 @@ public function testEmail() {
     /**************************************************************************/
 
     // Check basic email multiple.
-    $this->drupalGet('webform/test_element_email');
+    $this->drupalGet('/webform/test_element_email');
     $this->assertRaw('<label for="edit-email-multiple-basic">Multiple email addresses (basic)</label>');
     $this->assertRaw('<input data-drupal-selector="edit-email-multiple-basic" aria-describedby="edit-email-multiple-basic--description" type="text" id="edit-email-multiple-basic" name="email_multiple_basic" value="" size="60" class="form-text webform-email-multiple" />');
     $this->assertRaw('Multiple email addresses may be separated by commas.');
@@ -63,10 +63,11 @@ public function testEmail() {
     // email_confirm
     /**************************************************************************/
 
-    $this->drupalGet('webform/test_element_email');
+    $this->drupalGet('/webform/test_element_email');
 
     // Check basic email confirm.
-    $this->assertRaw('<div id="edit-email-confirm-basic" class="js-form-item form-item js-form-type-webform-email-confirm form-type-webform-email-confirm js-form-item-email-confirm-basic form-item-email-confirm-basic form-no-label">');
+    $this->assertRaw('<fieldset id="edit-email-confirm-basic--wrapper" class="webform-email-confirm--wrapper fieldgroup form-composite webform-composite-hidden-title js-webform-type-webform-email-confirm webform-type-webform-email-confirm js-form-item form-item js-form-wrapper form-wrapper">');
+    $this->assertRaw('<span class="visually-hidden fieldset-legend">Email confirm basic</span>');
     $this->assertRaw('<div class="js-form-item form-item js-form-type-email form-type-email js-form-item-email-confirm-basic-mail-1 form-item-email-confirm-basic-mail-1">');
     $this->assertRaw('<label for="edit-email-confirm-basic-mail-1">Email confirm basic</label>');
     $this->assertRaw('<input data-drupal-selector="edit-email-confirm-basic-mail-1" class="webform-email form-email" type="email" id="edit-email-confirm-basic-mail-1" name="email_confirm_basic[mail_1]" value="" size="60" maxlength="254" />');
@@ -75,7 +76,8 @@ public function testEmail() {
     $this->assertRaw('<input data-drupal-selector="edit-email-confirm-basic-mail-2" class="webform-email-confirm form-email" type="email" id="edit-email-confirm-basic-mail-2" name="email_confirm_basic[mail_2]" value="" size="60" maxlength="254" />');
 
     // Check advanced email confirm w/ custom label.
-    $this->assertRaw('<div id="edit-email-confirm-advanced" class="js-form-item form-item js-form-type-webform-email-confirm form-type-webform-email-confirm js-form-item-email-confirm-advanced form-item-email-confirm-advanced form-no-label">');
+    $this->assertRaw('<fieldset id="edit-email-confirm-advanced--wrapper" class="webform-email-confirm--wrapper fieldgroup form-composite webform-composite-hidden-title js-webform-type-webform-email-confirm webform-type-webform-email-confirm js-form-item form-item js-form-wrapper form-wrapper">');
+    $this->assertRaw('<span class="visually-hidden fieldset-legend">Email confirm advanced</span>');
     $this->assertRaw('<div class="js-form-item form-item js-form-type-email form-type-email js-form-item-email-confirm-advanced-mail-1 form-item-email-confirm-advanced-mail-1">');
     $this->assertRaw('<label for="edit-email-confirm-advanced-mail-1">Email confirm advanced</label>');
     $this->assertRaw('<input data-drupal-selector="edit-email-confirm-advanced-mail-1" aria-describedby="edit-email-confirm-advanced-mail-1--description" class="webform-email form-email" type="email" id="edit-email-confirm-advanced-mail-1" name="email_confirm_advanced[mail_1]" value="" size="60" maxlength="254" placeholder="Enter email address" />');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementEntityReferenceTest.php b/web/modules/webform/src/Tests/Element/WebformElementEntityReferenceTest.php
index 0838ec58a62eb76b486b0291b68010a799645507..16ad63446ec5629e5aedfb3d4389a22140417480 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementEntityReferenceTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementEntityReferenceTest.php
@@ -32,7 +32,7 @@ public function testEntityReferenceTest() {
     $webform = Webform::load('test_element_entity_reference');
 
     // Check render entity_autocomplete.
-    $this->drupalGet('webform/test_element_entity_reference');
+    $this->drupalGet('/webform/test_element_entity_reference');
     $this->assertFieldByName('entity_autocomplete_user_default', 'admin (1)');
     $this->assertFieldByName('entity_autocomplete_user_tags', 'admin (1)');
 
diff --git a/web/modules/webform/src/Tests/Element/WebformElementFieldsetTest.php b/web/modules/webform/src/Tests/Element/WebformElementFieldsetTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..9fdc13992c3932dc6d487a97809268ea74d0e19e
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementFieldsetTest.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+/**
+ * Tests for fieldset element.
+ *
+ * @group Webform
+ */
+class WebformElementFieldsetTest extends WebformElementTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_fieldset'];
+
+  /**
+   * Test fieldset element.
+   */
+  public function testFieldset() {
+    $this->drupalGet('/webform/test_element_fieldset');
+
+    // Check fieldset with help, field prefix, field suffix, description,
+    // and more. Also, check that invalid 'required' and 'aria-required'
+    // attributes are removed.
+    $this->assertRaw('<fieldset class="webform-has-field-prefix webform-has-field-suffix required js-webform-type-fieldset webform-type-fieldset js-form-item form-item js-form-wrapper form-wrapper" data-drupal-selector="edit-fieldset" aria-describedby="edit-fieldset--description" id="edit-fieldset">');
+    $this->assertRaw('<span class="fieldset-legend js-form-required form-required">fieldset<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;fieldset&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;This is help text.&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
+    $this->assertRaw('<span class="field-prefix">prefix</span>');
+    $this->assertRaw('<span class="field-suffix">suffix</span>');
+    $this->assertRaw('<div class="description">');
+    $this->assertRaw('<div id="edit-fieldset--description" class="webform-element-description">This is a description.</div>');
+    $this->assertRaw('<div id="edit-fieldset--more" class="js-webform-element-more webform-element-more">');
+
+    // Check fieldset title_display: invisible.
+    $this->assertRaw('<span class="visually-hidden fieldset-legend">fieldset_title_invisible</span>');
+
+    // Check fieldset description_display: before.
+    $this->assertRaw('<span class="field-prefix">prefix<div id="edit-fieldset-description-before--description" class="webform-element-description">This is a description before.</div>');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementFormatCustomTest.php b/web/modules/webform/src/Tests/Element/WebformElementFormatCustomTest.php
index a9962b49db443a06e5611b7c9124dafe9650538e..41947f82f39dc0ad643775beb566cc6396958c20 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementFormatCustomTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementFormatCustomTest.php
@@ -59,6 +59,16 @@ public function testFormatCustom() {
     $this->assertRaw('<label>textfield_custom</label>');
     $this->assertRaw('<em>{textfield_custom}</em>');
 
+    // Check basic custom token HTML format.
+    $this->assertRaw('<label>textfield_custom_token</label>');
+    $this->assertRaw('<em>{textfield_custom_token}</em>');
+
+    // Check caught exception is displayed to users with update access.
+    // @see \Drupal\webform\Twig\TwigExtension::renderTwigTemplate
+    $this->assertRaw('(&quot;The &quot;[webform_submission:values:textfield_custom_token_exception]&quot; token is being called recursively.&quot;)');
+    $this->assertRaw('<label>textfield_custom_token_exception</label>');
+    $this->assertRaw('<em>EXCEPTION</em>');
+
     // Check multiple custom HTML format.
     $this->assertRaw('<label>textfield_custom</label>');
     $this->assertRaw('<table>');
@@ -89,8 +99,19 @@ public function testFormatCustom() {
     $this->assertRaw('element.postal_code: {postal_code}<br/>');
     $this->assertRaw('element.country: {country}<br/>');
 
+    // Check composite multiple custom HTML format.
+    $this->assertRaw('<label>address_multiple_custom</label>');
+    $this->assertRaw('<div>*****</div>
+element.address: {02-address}<br/>
+element.address_2: {02-address_2}<br/>
+element.city: {02-city}<br/>
+element.state_province: {02-state_province}<br/>
+element.postal_code: {02-postal_code}<br/>
+element.country: {02-country}<br/>
+<div>*****</div>');
+
     // Check fieldset displayed as details.
-    $this->assertRaw('<details data-webform-element-id="test_element_format_custom--fieldset_custom" class="webform-container webform-container-type-details js-form-wrapper form-wrapper" id="test_element_format_custom--fieldset_custom" open="open">');
+    $this->assertRaw('<details class="webform-container webform-container-type-details js-form-wrapper form-wrapper" data-webform-element-id="test_element_format_custom--fieldset_custom" id="test_element_format_custom--fieldset_custom" open="open">');
     $this->assertRaw('<summary role="button" aria-controls="test_element_format_custom--fieldset_custom" aria-expanded="true" aria-pressed="true">fieldset_custom</summary>');
 
     // Check container custom HTML format.
@@ -102,6 +123,8 @@ public function testFormatCustom() {
 
     $this->drupalGet("admin/structure/webform/manage/test_element_format_custom/submission/$sid/text");
     $this->assertRaw("textfield_custom: /{textfield_custom}/
+textfield_custom_token: /{textfield_custom_token}/
+textfield_custom_token_exception: /EXCEPTION/
 textfield_custom:
 ⦿ One
 ⦿ Two
@@ -126,13 +149,34 @@ public function testFormatCustom() {
 element.postal_code: {postal_code}
 element.country: {country}
 
+address_multiple_custom:
+*****
+element.address: {01-address}
+element.address_2: {01-address_2}
+element.city: {01-city}
+element.state_province: {01-state_province}
+element.postal_code: {01-postal_code}
+element.country: {01-country}
+*****
+*****
+element.address: {02-address}
+element.address_2: {02-address_2}
+element.city: {02-city}
+element.state_province: {02-state_province}
+element.postal_code: {02-postal_code}
+element.country: {02-country}
+*****
+
+
 fieldset_custom
 ---------------
 fieldset_custom_textfield: {fieldset_custom_textfield}
 
 fieldset_custom_children
 ------------------------
-fieldset_custom_children_textfield: {fieldset_custom_children_textfield}");
+fieldset_custom_children_textfield: {fieldset_custom_children_textfield}
+
+");
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementFormatTest.php b/web/modules/webform/src/Tests/Element/WebformElementFormatTest.php
index 774d821af8045c86a4fac0febb6dd2e429fe60bb..3a6be5b90579bc0536af2b2497382362f1cc55da 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementFormatTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementFormatTest.php
@@ -79,11 +79,11 @@ public function testFormat() {
 //      'Entity autocomplete (Label (ID))' => 'admin (1)',
     ];
     foreach ($elements as $label => $value) {
-      $this->assertContains($body, '<b>' . $label . '</b><br />' . $value, new FormattableMarkup('Found @label: @value', ['@label' => $label, '@value' => $value]));
+      $this->assertContains('<b>' . $label . '</b><br />' . $value, $body, new FormattableMarkup('Found @label: @value', ['@label' => $label, '@value' => $value]));
     }
 
     // Check code format.
-    $this->assertContains($body, '<pre class="js-webform-codemirror-runmode webform-codemirror-runmode" data-webform-codemirror-mode="text/x-yaml">message: \'Hello World\'</pre>');
+    $this->assertContains('<pre class="js-webform-codemirror-runmode webform-codemirror-runmode" data-webform-codemirror-mode="text/x-yaml">message: \'Hello World\'</pre>', $body);
 
     // Check elements formatted as text.
     $body = $this->getMessageBody($submission, 'email_text');
@@ -110,7 +110,7 @@ public function testFormat() {
       'Time (Raw value): 09:00:00',
     ];
     foreach ($elements as $value) {
-      $this->assertContains($body, $value, new FormattableMarkup('Found @value', ['@value' => $value]));
+      $this->assertContains($value, $body, new FormattableMarkup('Found @value', ['@value' => $value]));
     }
 
     /**************************************************************************/
@@ -136,7 +136,7 @@ public function testFormat() {
       'File (URL)' => $this->getSubmissionFileUrl($submission, 'managed_file_url'),
     ];
     foreach ($elements as $label => $value) {
-      $this->assertContains($body, '<b>' . $label . '</b><br />' . $value, new FormattableMarkup('Found @label: @value', ['@label' => $label, '@value' => $value]));
+      $this->assertContains('<b>' . $label . '</b><br />' . $value, $body, new FormattableMarkup('Found @label: @value', ['@label' => $label, '@value' => $value]));
     }
 
     // Check managed file element formatted as text.
@@ -145,13 +145,13 @@ public function testFormat() {
       'File (Value): ' . $this->getSubmissionFileUrl($submission, 'managed_file_value'),
       'File (Raw value): ' . $this->getSubmissionFileUrl($submission, 'managed_file_raw'),
       'File (File): ' . $this->getSubmissionFileUrl($submission, 'managed_file_file'),
-      'File (Link): '  . $this->getSubmissionFileUrl($submission, 'managed_file_link'),
+      'File (Link): ' . $this->getSubmissionFileUrl($submission, 'managed_file_link'),
       'File (File ID): ' . $submission->getElementData('managed_file_id'),
       'File (File name): managed_file_name.txt',
       'File (URL): ' . $this->getSubmissionFileUrl($submission, 'managed_file_url'),
     ];
     foreach ($elements as $value) {
-      $this->assertContains($body, $value, new FormattableMarkup('Found @value', ['@value' => $value]));
+      $this->assertContains($value, $body, new FormattableMarkup('Found @value', ['@value' => $value]));
     }
 
     /**************************************************************************/
@@ -178,7 +178,10 @@ public function testFormat() {
       'Checkboxes (Unordered list)' => '<div class="item-list"><ul><li>One</li><li>Two</li><li>Three</li></ul>',
     ];
     foreach ($elements as $label => $value) {
-      $this->assertContains($body, '<b>' . $label . '</b><br />' . $value, new FormattableMarkup('Found @label: @value', ['@label' => $label, '@value' => $value]));
+      $this->assertContains('<b>' . $label . '</b><br />' . $value, $body, new FormattableMarkup('Found @label: @value', [
+        '@label' => $label,
+        '@value' => $value,
+      ]));
     }
 
     // Check elements formatted as text.
@@ -208,7 +211,7 @@ public function testFormat() {
 - Three',
     ];
     foreach ($elements as $value) {
-      $this->assertContains($body, $value, new FormattableMarkup('Found @value', ['@value' => $value]));
+      $this->assertContains($value, $body, new FormattableMarkup('Found @value', ['@value' => $value]));
     }
 
     /**************************************************************************/
@@ -232,7 +235,10 @@ public function testFormat() {
       'raw:' => '1, 2, 3',
     ];
     foreach ($elements as $label => $value) {
-      $this->assertContains($body, '<h3>' . $label . '</h3>' . $value . '<hr />', new FormattableMarkup('Found @label: @value', ['@label' => $label, '@value' => $value]));
+      $this->assertContains('<h3>' . $label . '</h3>' . $value . '<hr />', $body, new FormattableMarkup('Found @label: @value', [
+        '@label' => $label,
+        '@value' => $value,
+      ]));
     }
 
     // Check elements tokens formatted as text.
@@ -247,7 +253,7 @@ public function testFormat() {
       "raw:\n1, 2, 3",
     ];
     foreach ($elements as $value) {
-      $this->assertContains($body, $value, new FormattableMarkup('Found @value', ['@value' => $value]));
+      $this->assertContains($value, $body, new FormattableMarkup('Found @value', ['@value' => $value]));
     }
 
     // Check element default format item global setting.
@@ -255,14 +261,14 @@ public function testFormat() {
       ->set('format.checkboxes.item', 'raw')
       ->save();
     $body = $this->getMessageBody($webform_format_token_submission, 'email_text');
-    $this->assertContains($body, "default:\n1, 2, 3");
+    $this->assertContains("default:\n1, 2, 3", $body);
 
     // Check element default format items global setting.
     \Drupal::configFactory()->getEditable('webform.settings')
       ->set('format.checkboxes.items', 'and')
       ->save();
     $body = $this->getMessageBody($webform_format_token_submission, 'email_text');
-    $this->assertContains($body, "default:\n1, 2, and 3");
+    $this->assertContains("default:\n1, 2, and 3", $body);
   }
 
   /**
diff --git a/web/modules/webform/src/Tests/Element/WebformElementHelpTest.php b/web/modules/webform/src/Tests/Element/WebformElementHelpTest.php
index c13532788a7da56b62d7abbba744a7fb96166d9c..20e183f5dbafc915ff77e868900d013062d509ca 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementHelpTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementHelpTest.php
@@ -20,32 +20,38 @@ class WebformElementHelpTest extends WebformElementTestBase {
    * Test element help.
    */
   public function testHelp() {
-    $this->drupalGet('webform/test_element_help');
+    $this->drupalGet('/webform/test_element_help');
 
     // Check basic help.
-    $this->assertRaw('<a href="#help" title="{This is an example of help}" data-webform-help="{This is an example of help}" class="webform-element-help">?</a>');
+    $this->assertRaw('<label for="edit-help">help<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;help&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;{This is an example of help}&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
+
+    // Check help with required.
+    $this->assertRaw('<label for="edit-help-required" class="js-form-required form-required">help_required<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;help_required&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;{This is an example of help for a required element}&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
+
+    // Check help with custom title.
+    $this->assertRaw('<label for="edit-help-title">help_title<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;{Help custom title}&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;{This is an example of help with a custom help title}&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
 
     // Check help with HTML markup.
-    $this->assertRaw('<a href="#help" title="{This is an example of help with HTML markup}" data-webform-help="{This is an example of help with &lt;b&gt;HTML markup&lt;/b&gt;}" class="webform-element-help">?</a>');
+    $this->assertRaw('<label for="edit-help-html">help_html<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;help_html&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;{This is an example of help with &lt;b&gt;HTML markup&lt;/b&gt;}&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
 
     // Check help with XSS.
-    $this->assertRaw('<a href="#help" title="{This is an example of help with XSS alert(&quot;XSS&quot;)}" data-webform-help="{This is an example of help with &lt;b&gt;XSS alert(&quot;XSS&quot;)&lt;/b&gt;}" class="webform-element-help">?</a>');
+    $this->assertRaw('<label for="edit-help-xss">help_xss<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;help_xss&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;{This is an example of help with &lt;b&gt;XSS alert(&quot;XSS&quot;)&lt;/b&gt;}&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
 
     // Check help with inline title.
-    $this->assertRaw('<a href="#help" title="{This is an example of help with an inline title}" data-webform-help="{This is an example of help with an inline title}" class="webform-element-help">?</a>
-help_inline</label>');
+    $this->assertRaw('<label for="edit-help-checkbox" class="option">help_checkbox<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;help_checkbox&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;{This is an example of help}&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
+    $this->assertRaw('<label for="edit-help-inline"><span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;help_inline&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;{This is an example of help with an inline title}&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
 
     // Check radios (fieldset).
-    $this->assertRaw('<a href="#help" title="{This is an example of help for radio buttons}" data-webform-help="{This is an example of help for radio buttons}" class="webform-element-help">?</a>');
+    $this->assertRaw('<span class="fieldset-legend">help_radios<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;help_radios&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;{This is an example of help for radio buttons}&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
 
     // Check fieldset.
-    $this->assertRaw('<a href="#help" title="{This is an example of help for a fieldset}" data-webform-help="{This is an example of help for a fieldset}" class="webform-element-help">?</a>');
+    $this->assertRaw('<span class="fieldset-legend">help_radios<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;help_radios&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;{This is an example of help for radio buttons}&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
 
     // Check details.
-    $this->assertRaw('<a href="#help" title="{This is an example of help for a details element}" data-webform-help="{This is an example of help for a details element}" class="webform-element-help">?</a>');
+    $this->assertRaw('<summary role="button" aria-controls="edit-help-details" aria-expanded="false" aria-pressed="false">help_details<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;help_details&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;{This is an example of help for a details element}&lt;/div&gt;"><span aria-hidden="true">?</span>');
 
     // Check section.
-    $this->assertRaw('<a href="#help" title="{This is an example of help for a section element}" data-webform-help="{This is an example of help for a section element}" class="webform-element-help">?</a>');
+    $this->assertRaw('<h2 class="webform-section-title">help_section<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;help_section&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;{This is an example of help for a section element}&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementHorizontalRuleTest.php b/web/modules/webform/src/Tests/Element/WebformElementHorizontalRuleTest.php
index b767394eec7deccac56f1d1facdbbf53e4ece8b9..75051d3db0d4ce446124173f72ac6986b2789fc0 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementHorizontalRuleTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementHorizontalRuleTest.php
@@ -20,7 +20,7 @@ class WebformElementHorizontalRuleTest extends WebformElementTestBase {
    * Test horizontal rule element.
    */
   public function testHorizontalRule() {
-    $this->drupalGet('webform/test_element_horizontal_rule');
+    $this->drupalGet('/webform/test_element_horizontal_rule');
 
     // Check rendering.
     $this->assertRaw('<hr data-drupal-selector="edit-horizontal-rule" id="edit-horizontal-rule" class="webform-horizontal-rule" />');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementHtmlEditorTest.php b/web/modules/webform/src/Tests/Element/WebformElementHtmlEditorTest.php
index 3ee8e9866733c4b7d6f1f74947fd9dec633e8507..61d40dab39e67f46a9a6f660142c101a73c32b80 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementHtmlEditorTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementHtmlEditorTest.php
@@ -42,6 +42,8 @@ public function setUp() {
   public function testHtmlEditor() {
     $this->drupalLogin($this->rootUser);
 
+    /* Element text format */
+
     $webform = Webform::load('test_element_html_editor');
 
     // Check required validation.
@@ -56,11 +58,14 @@ public function testHtmlEditor() {
     $this->assertRaw('webform_html_editor (disable) field is required.');
     $this->assertRaw('webform_html_editor (format) field is required.');
     $this->assertRaw('webform_html_editor_codemirror (none) field is required.');
-    
-    $this->drupalGet('webform/test_element_html_editor');
+
+    $this->drupalGet('/webform/test_element_html_editor');
 
     // Check that HTML editor is enabled.
-    $this->assertRaw('<textarea class="js-html-editor form-textarea required resize-vertical" data-drupal-selector="edit-webform-html-editor-value" id="edit-webform-html-editor-value" name="webform_html_editor[value]" rows="5" cols="60" required="required" aria-required="true">Hello &lt;b&gt;World!!!&lt;/b&gt;</textarea>');
+    $this->assertRaw('<textarea data-drupal-selector="edit-webform-html-editor-value" class="js-html-editor form-textarea required resize-vertical" id="edit-webform-html-editor-value" name="webform_html_editor[value]" rows="5" cols="60" required="required" aria-required="true">Hello &lt;b&gt;World!!!&lt;/b&gt;</textarea>');
+
+    // Check that HTML editor is disabled.
+    $this->assertRaw('<textarea class="custom-disabled js-html-editor form-textarea required resize-vertical" data-drupal-selector="edit-webform-html-editor-disable-value" id="edit-webform-html-editor-disable-value" name="webform_html_editor_disable[value]" rows="5" cols="60" required="required" aria-required="true">Hello &lt;b&gt;World!!!&lt;/b&gt;</textarea>');
 
     // Check that CodeMirror is displayed when #format: FALSE.
     $this->assertRaw('<textarea data-drupal-selector="edit-webform-html-editor-codemirror-value" class="js-webform-codemirror webform-codemirror html required form-textarea resize-vertical" required="required" aria-required="true" data-webform-codemirror-mode="text/html" id="edit-webform-html-editor-codemirror-value" name="webform_html_editor_codemirror[value]" rows="5" cols="60">Hello &lt;b&gt;World!!!&lt;/b&gt;</textarea>');
@@ -69,26 +74,29 @@ public function testHtmlEditor() {
     $this->drupalPostForm('admin/structure/webform/config/elements', ['html_editor[disabled]' => TRUE], t('Save configuration'));
 
     // Check that HTML editor is removed and replaced by CodeMirror HTML editor.
-    $this->drupalGet('webform/test_element_html_editor');
+    $this->drupalGet('/webform/test_element_html_editor');
     $this->assertNoRaw('<textarea class="js-html-editor form-textarea required resize-vertical" data-drupal-selector="edit-webform-html-editor-value" id="edit-webform-html-editor-value" name="webform_html_editor[value]" rows="5" cols="60" required="required" aria-required="true">Hello &lt;b&gt;World!!!&lt;/b&gt;</textarea>');
     $this->assertRaw('<textarea data-drupal-selector="edit-webform-html-editor-value" class="js-webform-codemirror webform-codemirror html required form-textarea resize-vertical" required="required" aria-required="true" data-webform-codemirror-mode="text/html" id="edit-webform-html-editor-value" name="webform_html_editor[value]" rows="5" cols="60">Hello &lt;b&gt;World!!!&lt;/b&gt;</textarea>');
 
-    // Enable HTML editor and text format.
+    // Enable HTML editor and element text format.
     $edit = [
       'html_editor[disabled]' => FALSE,
-      'html_editor[format]' => 'basic_html',
+      'html_editor[element_format]' => 'basic_html',
     ];
     $this->drupalPostForm('admin/structure/webform/config/elements', $edit, t('Save configuration'));
 
     // Check that Text format is disabled.
-    $this->drupalGet('webform/test_element_html_editor');
+    $this->drupalGet('/webform/test_element_html_editor');
     $this->assertNoRaw('<textarea class="js-html-editor form-textarea resize-vertical" data-drupal-selector="edit-webform-html-editor-value" id="edit-webform-html-editor-value" name="webform_html_editor[value]" rows="5" cols="60">Hello &lt;b&gt;World!!!&lt;/b&gt;</textarea>');
     $this->assertNoRaw('<textarea data-drupal-selector="edit-webform-html-editor-value" class="js-webform-codemirror webform-codemirror html required form-textarea resize-vertical" required="required" aria-required="true" data-webform-codemirror-mode="text/html" id="edit-webform-html-editor-value" name="webform_html_editor[value]" rows="5" cols="60">Hello &lt;b&gt;World!!!&lt;/b&gt;</textarea>');
     $this->assertRaw('<textarea data-drupal-selector="edit-webform-html-editor-value-value" id="edit-webform-html-editor-value-value" name="webform_html_editor[value][value]" rows="5" cols="60" class="form-textarea required resize-vertical" required="required" aria-required="true">Hello &lt;b&gt;World!!!&lt;/b&gt;</textarea>');
     $this->assertRaw('<h4 class="label">Basic HTML</h4>');
 
-    // Disable text format.
-    $this->drupalPostForm('admin/structure/webform/config/elements', ['html_editor[format]' => ''], t('Save configuration'));
+    // Disable element text format.
+    $edit = [
+      'html_editor[element_format]' => '',
+    ];
+    $this->drupalPostForm('admin/structure/webform/config/elements', $edit, t('Save configuration'));
 
     // Check that tidy removed <p> tags.
     $build = WebformHtmlEditor::checkMarkup('<p>Some text</p>');
@@ -106,6 +114,31 @@ public function testHtmlEditor() {
     // Check that tidy is disabled.
     $build = WebformHtmlEditor::checkMarkup('<p>Some text</p>');
     $this->assertEqual(\Drupal::service('renderer')->renderPlain($build), '<p>Some text</p>');
+
+    /* Email text format */
+    // Disable HTML editor.
+    $edit = [
+      'html_editor[disabled]' => FALSE,
+      'html_editor[element_format]' => '',
+      'html_editor[mail_format]' => '',
+    ];
+    $this->drupalPostForm('admin/structure/webform/config/elements', $edit, t('Save configuration'));
+
+    // Check that HTML editor is used.
+    $this->drupalGet('/admin/structure/webform/manage/contact/handlers/email_confirmation/edit');
+    $this->assertRaw('<textarea data-drupal-selector="edit-settings-body-custom-html-value" class="js-html-editor form-textarea resize-vertical" id="edit-settings-body-custom-html-value" name="settings[body_custom_html][value]" rows="5" cols="60">');
+
+    // Enable mail text format.
+    $edit = [
+      'html_editor[mail_format]' => 'basic_html',
+    ];
+    $this->drupalPostForm('admin/structure/webform/config/elements', $edit, t('Save configuration'));
+
+    // Check mail text format is used.
+    $this->drupalGet('/admin/structure/webform/manage/contact/handlers/email_confirmation/edit');
+    $this->assertNoRaw('<textarea data-drupal-selector="edit-settings-body-custom-html-value" class="js-html-editor form-textarea resize-vertical" id="edit-settings-body-custom-html-value" name="settings[body_custom_html][value]" rows="5" cols="60">');
+    $this->assertRaw('<textarea data-drupal-selector="edit-settings-body-custom-html-value-value" id="edit-settings-body-custom-html-value-value" name="settings[body_custom_html][value][value]" rows="5" cols="60" class="form-textarea resize-vertical">');
+    $this->assertRaw('<div class="filter-wrapper js-form-wrapper form-wrapper" data-drupal-selector="edit-settings-body-custom-html-value-format" id="edit-settings-body-custom-html-value-format">');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementIcheckTest.php b/web/modules/webform/src/Tests/Element/WebformElementIcheckTest.php
index 112d14699a6430e278fff3e89bd08317c3ff7a39..24a2c9d927aae735de245ba0967cda0c4b457029 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementIcheckTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementIcheckTest.php
@@ -31,7 +31,7 @@ public function setUp() {
    */
   public function testIcheckElement() {
 
-    $this->drupalGet('webform/test_element_icheck');
+    $this->drupalGet('/webform/test_element_icheck');
 
     // Check custom iCheck style set to 'flat'.
     $this->assertRaw('<div class="js-form-item form-item js-form-type-checkbox form-type-checkbox js-form-item-checkbox-custom form-item-checkbox-custom">');
@@ -50,7 +50,7 @@ public function testIcheckElement() {
       ->set('element.default_icheck', 'minimal')
       ->save();
 
-    $this->drupalGet('webform/test_element_icheck');
+    $this->drupalGet('/webform/test_element_icheck');
 
     // Check custom iCheck style still set to 'flat'.
     $this->assertRaw('<div class="js-form-item form-item js-form-type-checkbox form-type-checkbox js-form-item-checkbox-custom form-item-checkbox-custom">');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementImageResolutionTest.php b/web/modules/webform/src/Tests/Element/WebformElementImageResolutionTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..eff6bd3f2c153b23a394d6b32193171c270f3623
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementImageResolutionTest.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+/**
+ * Tests for webform image resolution element.
+ *
+ * @group Webform
+ */
+class WebformElementImageResolutionTest extends WebformElementTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_image_resolution'];
+
+  /**
+   * Tests image resolution element.
+   */
+  public function testImageResolution() {
+
+    $this->drupalGet('/webform/test_element_image_resolution');
+
+    // Check rendering.
+    $this->assertRaw('<label for="edit-webform-image-resolution-advanced">webform_image_resolution_advanced</label>');
+    $this->assertRaw('<label for="edit-webform-image-resolution-advanced-x" class="visually-hidden">{width_title}</label>');
+    $this->assertRaw('<input data-drupal-selector="edit-webform-image-resolution-advanced-x" type="number" id="edit-webform-image-resolution-advanced-x" name="webform_image_resolution_advanced[x]" value="300" step="1" min="1" class="form-number" />');
+    $this->assertRaw('<label for="edit-webform-image-resolution-advanced-y" class="visually-hidden">{height_title}</label>');
+    $this->assertRaw('<input data-drupal-selector="edit-webform-image-resolution-advanced-y" type="number" id="edit-webform-image-resolution-advanced-y" name="webform_image_resolution_advanced[y]" value="400" step="1" min="1" class="form-number" />');
+    $this->assertRaw('{description}');
+
+    // Check validation.
+    $this->drupalPostForm('webform/test_element_image_resolution', ['webform_image_resolution[x]' => '100'], t('Submit'));
+    $this->assertRaw('Both a height and width value must be specified in the webform_image_resolution field.');
+
+    // Check processing.
+    $this->drupalPostForm('webform/test_element_image_resolution', [], t('Submit'));
+    $this->assertRaw("webform_image_resolution: ''
+webform_image_resolution_advanced: 300x400");
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementInputMaskTest.php b/web/modules/webform/src/Tests/Element/WebformElementInputMaskTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0cc910463be4162aba4680594d0dd8778a9066e4
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementInputMaskTest.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+use Drupal\webform\Entity\Webform;
+
+/**
+ * Tests for element input mask.
+ *
+ * @group Webform
+ */
+class WebformElementInputMaskTest extends WebformElementTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_input_mask'];
+
+  /**
+   * Test element input mask.
+   */
+  public function testInputMask() {
+    $webform = Webform::load('test_element_input_mask');
+
+    // Check default values.
+    $this->postSubmission($webform);
+    $this->assertRaw("currency: ''
+datetime: ''
+decimal: ''
+email: ''
+ip: ''
+license_plate: ''
+mac: ''
+percentage: ''
+phone: ''
+ssn: ''
+vin: ''
+zip: ''
+uppercase: ''
+lowercase: ''
+custom: ''");
+
+    // Check patterns.
+    $edit = [
+      'email' => 'example@example.com',
+      'datetime' => '2007-06-09\'T\'17:46:21',
+      'decimal' => '9.9',
+      'ip' => '255.255.255.255',
+      'currency' => '$ 9.99',
+      'percentage' => '99 %',
+      'phone' => '(999) 999-9999',
+      'license_plate' => '9-AAA-999',
+      'mac' => '99-99-99-99-99-99',
+      'ssn' => '999-99-9999',
+      'vin' => 'JA3AY11A82U020534',
+      'zip' => '99999-9999',
+      'uppercase' => 'UPPERCASE',
+      'lowercase' => 'lowercase',
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw("currency: '$ 9.99'
+datetime: '2007-06-09''T''17:46:21'
+decimal: '9.9'
+email: example@example.com
+ip: 255.255.255.255
+license_plate: 9-AAA-999
+mac: 99-99-99-99-99-99
+percentage: '99 %'
+phone: '(999) 999-9999'
+ssn: 999-99-9999
+vin: JA3AY11A82U020534
+zip: 99999-9999
+uppercase: UPPERCASE
+lowercase: lowercase
+custom: ''");
+
+    // Check pattern validation error messages.
+    $edit = [
+      'currency' => '$ 9.9_',
+      'decimal' => '9._',
+      'ip' => '255.255.255.__',
+      'mac' => '99-99-99-99-99-_)',
+      'percentage' => '_ %',
+      'phone' => '(999) 999-999_',
+      'ssn' => '999-99-999_',
+      'zip' => '99999-999_',
+    ];
+    $this->postSubmission($webform, $edit);
+    foreach ($edit as $name => $value) {
+      $this->assertRaw('<em class="placeholder">' . $name . '</em> field is not in the right format.');
+    }
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementLikertTest.php b/web/modules/webform/src/Tests/Element/WebformElementLikertTest.php
index 758d75c774c22892a739b6febb8f5a94ff5654d5..0a1d7c7ef2dbc06f2fc4c4c994e4b6431ae68388 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementLikertTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementLikertTest.php
@@ -21,33 +21,33 @@ class WebformElementLikertTest extends WebformElementTestBase {
    */
   public function testLikertElement() {
 
-    $this->drupalGet('webform/test_element_likert');
+    $this->drupalGet('/webform/test_element_likert');
 
     // Check default likert element.
-    $this->assertRaw('<table class="webform-likert-table responsive-enabled" data-likert-answers-count="3" data-drupal-selector="edit-likert-default-table" id="edit-likert-default-table" data-striping="1">');
-    $this->assertPattern('#<tr>\s+<th></th>\s+<th>Option 1</th>\s+<th>Option 2</th>\s+<th>Option 3</th>\s+</tr>#');
-    $this->assertRaw('<label for="edit-likert-default-table-q1-likert-question">Question 1</label>');
+    $this->assertRaw('<table class="webform-likert-table sticky-enabled responsive-enabled" data-likert-answers-count="3" data-drupal-selector="edit-likert-default-table" id="edit-likert-default-table" data-striping="1">');
+    $this->assertPattern('#<tr>\s+<th><span class="visually-hidden">Questions</span></th>\s+<th>Option 1</th>\s+<th>Option 2</th>\s+<th>Option 3</th>\s+</tr>#');
+    $this->assertRaw('<label>Question 1</label>');
     $this->assertRaw('<td><div class="js-form-item form-item js-form-type-radio form-type-radio js-form-item-likert-default-q1 form-item-likert-default-q1">');
-    $this->assertRaw('<input data-drupal-selector="edit-likert-default-q1" type="radio" id="edit-likert-default-q1" name="likert_default[q1]" value="1" class="form-radio" />');
-    $this->assertRaw('<label for="edit-likert-default-q1" class="option"><span class="webform-likert-label">Option 1</span></label>');
+    $this->assertRaw('<input aria-labelledby="edit-likert-default-table-q1-likert-question" data-drupal-selector="edit-likert-default-q1" type="radio" id="edit-likert-default-q1" name="likert_default[q1]" value="1" class="form-radio" />');
+    $this->assertRaw('<label for="edit-likert-default-q1" class="option"><span class="webform-likert-label visually-hidden">Option 1</span></label>');
 
     // Check advanced likert element with N/A.
-    $this->assertPattern('#<tr>\s+<th></th>\s+<th>Option 1</th>\s+<th>Option 2</th>\s+<th>Option 3</th>\s+<th>Not applicable</th>\s+</tr>#');
+    $this->assertPattern('#<tr>\s+<th><span class="visually-hidden">Questions</span></th>\s+<th>Option 1</th>\s+<th>Option 2</th>\s+<th>Option 3</th>\s+<th>Not applicable</th>\s+</tr>#');
     $this->assertRaw('<td><div class="js-form-item form-item js-form-type-radio form-type-radio js-form-item-likert-advanced-q1 form-item-likert-advanced-q1">');
-    $this->assertRaw('<input data-drupal-selector="edit-likert-advanced-q1" type="radio" id="edit-likert-advanced-q1--4" name="likert_advanced[q1]" value="N/A" class="form-radio" />');
-    $this->assertRaw('<label for="edit-likert-advanced-q1--4" class="option"><span class="webform-likert-label">Not applicable</span></label>');
+    $this->assertRaw('<input aria-labelledby="edit-likert-advanced-table-q1-likert-question" data-drupal-selector="edit-likert-advanced-q1" type="radio" id="edit-likert-advanced-q1--4" name="likert_advanced[q1]" value="N/A" class="form-radio" />');
+    $this->assertRaw('<label for="edit-likert-advanced-q1--4" class="option"><span class="webform-likert-label visually-hidden">Not applicable</span></label>');
 
     // Check likert with description.
     $this->assertRaw('<th>Option 1<div class="description">This is a description</div>');
-    $this->assertRaw('<label for="edit-likert-description-table-q1-likert-question">Question 1</label>');
-    $this->assertRaw('<div id="edit-likert-description-table-q1-likert-question--description" class="description">');
-    $this->assertRaw('<label for="edit-likert-description-q1" class="option"><span class="webform-likert-label">Option 1</span></label>');
-    $this->assertRaw('<span class="webform-likert-description">This is a description</span>');
+    $this->assertRaw('<label>Question 1</label>');
+    $this->assertRaw('<div id="edit-likert-description-table-q1-likert-question--description" class="webform-element-description">');
+    $this->assertRaw('<label for="edit-likert-description-q1" class="option"><span class="webform-likert-label visually-hidden">Option 1</span></label>');
+    $this->assertRaw('<span class="webform-likert-description hidden">This is a description</span>');
 
     // Check likert with help.
-    $this->assertRaw('<th>Option 1<a href="#help" title="This is a help text" data-webform-help="This is a help text" class="webform-element-help">?</a>');
-    $this->assertRaw('<label for="edit-likert-help-table-q1-likert-question">Question 1<a href="#help" title="This is a help text" data-webform-help="This is a help text" class="webform-element-help">?</a>');
-    $this->assertRaw('<label for="edit-likert-help-q1--2" class="option"><span class="webform-likert-label">Option 2<a href="#help" title="This is a help text" data-webform-help="This is a help text" class="webform-element-help">?</a>');
+    $this->assertRaw('<th>Option 1<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;Option 1&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;This is help text&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
+    $this->assertRaw('<label>Question 1<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;Question 1&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;This is help text&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
+    $this->assertRaw('<label for="edit-likert-help-q1--2" class="option"><span class="webform-likert-label visually-hidden">Option 2<span class="webform-likert-help hidden"><span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;Option 2&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;This is help text&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
 
     // Check likert required.
     $this->drupalPostForm('webform/test_element_likert', [], t('Submit'));
diff --git a/web/modules/webform/src/Tests/Element/WebformElementLocationPlacesTest.php b/web/modules/webform/src/Tests/Element/WebformElementLocationPlacesTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..47eab668a43ddd9ec8bf720db8024b074732334f
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementLocationPlacesTest.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+use Drupal\webform\Entity\Webform;
+
+/**
+ * Tests for location (Algolia) places element.
+ *
+ * @group Webform
+ */
+class WebformElementLocationPlacesTest extends WebformElementTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_loc_places'];
+
+  /**
+   * Test location (Algolia) places element.
+   */
+  public function testLocationPlaces() {
+    $webform = Webform::load('test_element_loc_places');
+
+    $this->drupalGet('/webform/test_element_loc_places');
+
+    // Check hidden attributes.
+    $this->assertRaw('<input class="webform-location-places form-text" data-drupal-selector="edit-location-default-value" type="text" id="edit-location-default-value" name="location_default[value]" value="" size="60" maxlength="255" placeholder="Enter a location" />');
+    $this->assertRaw('<input data-webform-location-places-attribute="lat" data-drupal-selector="edit-location-default-lat" type="hidden" name="location_default[lat]" value="" />');
+    $this->assertRaw('<input data-webform-location-places-attribute="lng" data-drupal-selector="edit-location-default-lng" type="hidden" name="location_default[lng]" value="" />');
+    $this->assertRaw('<input data-webform-location-places-attribute="name" data-drupal-selector="edit-location-default-name" type="hidden" name="location_default[name]" value="" />');
+    $this->assertRaw('<input data-webform-location-places-attribute="city" data-drupal-selector="edit-location-default-city" type="hidden" name="location_default[city]" value="" />');
+    $this->assertRaw('<input data-webform-location-places-attribute="country" data-drupal-selector="edit-location-default-country" type="hidden" name="location_default[country]" value="" />');
+    $this->assertRaw('<input data-webform-location-places-attribute="country_code" data-drupal-selector="edit-location-default-country-code" type="hidden" name="location_default[country_code]" value="" />');
+    $this->assertRaw('<input data-webform-location-places-attribute="administrative" data-drupal-selector="edit-location-default-administrative" type="hidden" name="location_default[administrative]" value="" />');
+    $this->assertRaw('<input data-webform-location-places-attribute="county" data-drupal-selector="edit-location-default-county" type="hidden" name="location_default[county]" value="" />');
+    $this->assertRaw('<input data-webform-location-places-attribute="suburb" data-drupal-selector="edit-location-default-suburb" type="hidden" name="location_default[suburb]" value="" />');
+    $this->assertRaw('<input data-webform-location-places-attribute="postcode" data-drupal-selector="edit-location-default-postcode" type="hidden" name="location_default[postcode]" value="" />');
+
+    // Check visible attributes.
+    $this->assertRaw('<input class="webform-location-places form-text" data-drupal-selector="edit-location-attributes-value" type="text" id="edit-location-attributes-value" name="location_attributes[value]" value="" size="60" maxlength="255" />');
+    $this->assertRaw('<input data-webform-location-places-attribute="lat" data-drupal-selector="edit-location-attributes-lat" type="text" id="edit-location-attributes-lat" name="location_attributes[lat]" value="" size="60" maxlength="255" class="form-text" />');
+
+    // Check invalid validation.
+    $edit = [
+      'location_attributes_required[value]' => 'test',
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('The location_attributes_required is not valid.');
+
+    // Check valid validation with lat(itude).
+    $edit = [
+      'location_attributes_required[value]' => 'test',
+      'location_attributes_required[lat]' => 1,
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertNoRaw('The location_attributes_required is not valid.');
+
+    // Check application id and API key is missing.
+    $this->assertNoRaw('"app_id"');
+    $this->assertNoRaw('"api_key"');
+
+    // Set application id and API key.
+    \Drupal::configFactory()->getEditable('webform.settings')
+      ->set('element.default_algolia_places_app_id', '{default_algolia_places_app_id}')
+      ->set('element.default_algolia_places_api_key', '{default_algolia_places_api_key}')
+      ->save();
+
+    // Check application id and API key is set.
+    $this->drupalGet('/webform/test_element_loc_places');
+    $this->assertRaw('"app_id"');
+    $this->assertRaw('"api_key"');
+    $this->assertRaw('"webform":{"location":{"places":{"app_id":"{default_algolia_places_app_id}","api_key":"{default_algolia_places_api_key}"}}}');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementManagedFileImageTest.php b/web/modules/webform/src/Tests/Element/WebformElementManagedFileImageTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..846ae67ccd653baca51d3856a83f95fd064f9740
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementManagedFileImageTest.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+use Drupal\webform\Entity\Webform;
+
+/**
+ * Test for webform element managed file image handling.
+ *
+ * @group Webform
+ */
+class WebformElementManagedFileImageTest extends WebformElementManagedFileTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['file', 'image', 'webform'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_image_file'];
+
+  /**
+   * Test image file upload.
+   */
+  public function testImageFileUpload() {
+    $this->drupalLogin($this->rootUser);
+
+    $webform = Webform::load('test_element_image_file');
+
+    // Get test image.
+    $images = $this->drupalGetTestFiles('image');
+    $image = reset($images);
+
+    // Check image max resolution validation is being applied.
+    $edit = [
+      'files[webform_image_file_advanced]' => \Drupal::service('file_system')->realpath($image->uri),
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('The image was resized to fit within the maximum allowed dimensions of <em class="placeholder">20x20</em> pixels.');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementManagedFileLimitTest.php b/web/modules/webform/src/Tests/Element/WebformElementManagedFileLimitTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..901fb11427c7783f5c61763f5165234e25442d5d
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementManagedFileLimitTest.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+use Drupal\webform\Entity\Webform;
+
+/**
+ * Test for webform element managed file limit.
+ *
+ * @group Webform
+ */
+class WebformElementManagedFileLimitTest extends WebformElementManagedFileTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['file', 'webform'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_managed_file_limit'];
+
+  /**
+   * Test file limit.
+   */
+  public function testLimits() {
+    $this->drupalLogin($this->rootUser);
+
+    // Get a 1 MB text file.
+    $files = $this->getTestFiles('text', '1024');
+    $file = reset($files);
+    $bytes = filesize($file->uri);
+    $this->debug($bytes);
+
+    $webform = Webform::load('test_element_managed_file_limit');
+
+    // Check form file limit.
+    $this->drupalGet('/webform/test_element_managed_file_limit');
+    $this->assertRaw('1 MB limit per form.');
+
+    // Check empty form file limit.
+    $webform->setSetting('form_file_limit', '')->save();
+    $this->drupalGet('/webform/test_element_managed_file_limit');
+    $this->assertNoRaw('1 MB limit per form.');
+
+    // Check default form file limit.
+    \Drupal::configFactory()
+      ->getEditable('webform.settings')
+      ->set('settings.default_form_file_limit', '2 MB')
+      ->save();
+    $this->drupalGet('/webform/test_element_managed_file_limit');
+    $this->assertRaw('2 MB limit per form.');
+
+    // Set limit to 2 files.
+    \Drupal::configFactory()
+      ->getEditable('webform.settings')
+      ->set('settings.default_form_file_limit', ($bytes * 2) . ' bytes')
+      ->save();
+    $this->drupalGet('/webform/test_element_managed_file_limit');
+    $this->assertRaw(format_size($bytes * 2) . ' limit per form.');
+
+    // Check valid file upload.
+    $edit = [
+      'files[managed_file_01]' => \Drupal::service('file_system')->realpath($file->uri),
+    ];
+    $sid = $this->postSubmission($webform, $edit);
+    $this->assertNotNull($sid);
+
+    // Check invalid file upload.
+    $edit = [
+      'files[managed_file_01]' => \Drupal::service('file_system')->realpath($file->uri),
+      'files[managed_file_02]' => \Drupal::service('file_system')->realpath($file->uri),
+      'files[managed_file_03]' => \Drupal::service('file_system')->realpath($file->uri),
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('This form\'s file upload quota of <em class="placeholder">2 KB</em> has been exceeded. Please remove some files.');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementManagedFilePreviewTest.php b/web/modules/webform/src/Tests/Element/WebformElementManagedFilePreviewTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..9a6008fc6c7d1109890069cbc99130a742097947
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementManagedFilePreviewTest.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+/**
+ * Test for webform element managed file preview.
+ *
+ * @group Webform
+ */
+class WebformElementManagedFilePreviewTest extends WebformElementManagedFileTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['file', 'image', 'webform'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_managed_file_prev'];
+
+  /**
+   * Test image file upload.
+   */
+  public function testImageFileUpload() {
+    global $base_url;
+
+    // Check that anonymous users can not preview files.
+    $this->drupalGet('/webform/test_element_managed_file_prev/test');
+    $this->assertRaw('<span data-drupal-selector="edit-webform-image-file-file-1-filename" class="file file--mime-image-gif file--image"> webform_image_file.gif</span>');
+    $this->assertRaw('<span data-drupal-selector="edit-webform-audio-file-file-3-filename" class="file file--mime-audio-mpeg file--audio"> webform_audio_file.mp3</span>');
+    $this->assertRaw('<span data-drupal-selector="edit-webform-video-file-file-5-filename" class="file file--mime-video-mp4 file--video"> webform_video_file.mp4</span>');
+    $this->assertRaw('<span data-drupal-selector="edit-webform-document-file-file-7-filename" class="file file--mime-text-plain file--text"> webform_document_file.txt</span>');
+
+    // Login admin user.
+    $this->drupalLogin($this->rootUser);
+
+    // Check that authenticated users can preview files.
+    $this->drupalGet('/webform/test_element_managed_file_prev/test');
+
+    $this->assertRaw('<div class="webform-managed-file-preview webform-image-file-preview js-form-wrapper form-wrapper" data-drupal-selector="edit-webform-image-file-file-9-filename" id="edit-webform-image-file-file-9-filename">');
+    $this->assertRaw('<a href="' . $base_url . '/system/files/webform/test_element_managed_file_prev/_sid_/webform_image_file_0.gif" class="js-webform-image-file-modal webform-image-file-modal">');
+
+    $this->assertRaw('<div class="webform-managed-file-preview webform-audio-file-preview js-form-wrapper form-wrapper" data-drupal-selector="edit-webform-audio-file-file-11-filename" id="edit-webform-audio-file-file-11-filename">');
+    $this->assertRaw('<source src="' . $base_url . '/system/files/webform/test_element_managed_file_prev/_sid_/webform_audio_file_0.mp3" type="audio/mpeg">');
+
+    $this->assertRaw('<div class="webform-managed-file-preview webform-video-file-preview js-form-wrapper form-wrapper" data-drupal-selector="edit-webform-video-file-file-13-filename" id="edit-webform-video-file-file-13-filename">');
+    $this->assertRaw('<source src="' . $base_url . '/system/files/webform/test_element_managed_file_prev/_sid_/webform_video_file_0.mp4" type="video/mp4">');
+
+    $this->assertRaw('<div class="webform-managed-file-preview webform-document-file-preview js-form-wrapper form-wrapper" data-drupal-selector="edit-webform-document-file-file-15-filename" id="edit-webform-document-file-file-15-filename">');
+    $this->assertRaw($base_url . '/system/files/webform/test_element_managed_file_prev/_sid_/webform_document_file_0.txt');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementManagedFilePrivateTest.php b/web/modules/webform/src/Tests/Element/WebformElementManagedFilePrivateTest.php
index fd30c71dffc60b309168a48605e9716cf973b317..8a829e30ca692343a97dae16f02bdd919a20a7a2 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementManagedFilePrivateTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementManagedFilePrivateTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Url;
 use Drupal\file\Entity\File;
+use Drupal\webform\Entity\Webform;
 use Drupal\webform\Entity\WebformSubmission;
 
 /**
@@ -13,22 +14,37 @@
  */
 class WebformElementManagedFilePrivateTest extends WebformElementManagedFileTestBase {
 
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_managed_file'];
+
   /**
    * Test private files.
    */
   public function testPrivateFiles() {
-    $elements = $this->webform->getElementsDecoded();
+    $admin_submission_user = $this->drupalCreateUser([
+      'administer webform submission',
+    ]);
+
+    $webform = Webform::load('test_element_managed_file');
+
+    /**************************************************************************/
+
+    $elements = $webform->getElementsDecoded();
     $elements['managed_file_single']['#uri_scheme'] = 'private';
-    $this->webform->setElements($elements);
-    $this->webform->save();
+    $webform->setElements($elements);
+    $webform->save();
 
-    $this->drupalLogin($this->adminSubmissionUser);
+    $this->drupalLogin($admin_submission_user);
 
     // Upload private file as authenticated user.
     $edit = [
       'files[managed_file_single]' => \Drupal::service('file_system')->realpath($this->files[0]->uri),
     ];
-    $sid = $this->postSubmission($this->webform, $edit);
+    $sid = $this->postSubmission($webform, $edit);
 
     /** @var \Drupal\webform\WebformSubmissionInterface $submission */
     $submission = WebformSubmission::load($sid);
@@ -59,17 +75,19 @@ public function testPrivateFiles() {
     $destination_url = Url::fromUri('base://system/files', [
       'query' => [
         'file' => 'webform/test_element_managed_file/' . $sid . '/' . $this->files[0]->filename,
-      ]
+      ],
+    ]);
+    $this->assertUrl('user/login', [
+      'query' => [
+        'destination' => $destination_url->toString(),
+      ],
     ]);
-    $this->assertUrl('user/login', ['query' => [
-      'destination' => $destination_url->toString(),
-    ]]);
 
     // Upload private file and preview as anonymous user.
     $edit = [
       'files[managed_file_single]' => \Drupal::service('file_system')->realpath($this->files[1]->uri),
     ];
-    $this->drupalPostForm('webform/' . $this->webform->id(), $edit, t('Preview'));
+    $this->drupalPostForm('webform/' . $webform->id(), $edit, t('Preview'));
 
     $temp_file_uri = file_create_url('private://webform/test_element_managed_file/_sid_/' . basename($this->files[1]->uri));
 
@@ -82,7 +100,7 @@ public function testPrivateFiles() {
     $this->assertResponse(403);
 
     // Check that authenticated user can't access temp file.
-    $this->drupalLogin($this->adminSubmissionUser);
+    $this->drupalLogin($admin_submission_user);
     $this->drupalGet($temp_file_uri);
     $this->assertResponse(403);
 
diff --git a/web/modules/webform/src/Tests/Element/WebformElementManagedFilePublicTest.php b/web/modules/webform/src/Tests/Element/WebformElementManagedFilePublicTest.php
index cbcc511bf55f556f5183f3f77bedfab9aff6b3cd..f471aa52518b76e9ef21e33b04412a937ab5a83d 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementManagedFilePublicTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementManagedFilePublicTest.php
@@ -48,7 +48,7 @@ public function testPublicUpload() {
     $this->drupalLogin($this->rootUser);
 
     // Check element webform warning message for public files.
-    $this->drupalGet('admin/structure/webform/manage/test_element_managed_file/element/managed_file_single/edit');
+    $this->drupalGet('/admin/structure/webform/manage/test_element_managed_file/element/managed_file_single/edit');
     $this->assertRaw('Public files upload destination is dangerous for webforms that are available to anonymous and/or untrusted users.');
     $this->assertFieldById('edit-properties-uri-scheme-public');
 
@@ -56,7 +56,7 @@ public function testPublicUpload() {
     \Drupal::configFactory()->getEditable('webform.settings')
       ->set('file.file_public', FALSE)
       ->save();
-    $this->drupalGet('admin/structure/webform/manage/test_element_managed_file/element/managed_file_single/edit');
+    $this->drupalGet('/admin/structure/webform/manage/test_element_managed_file/element/managed_file_single/edit');
     $this->assertNoRaw('Public files upload destination is dangerous for webforms that are available to anonymous and/or untrusted users.');
     $this->assertNoFieldById('edit-properties-uri-scheme-public');
 
@@ -66,7 +66,7 @@ public function testPublicUpload() {
     /**************************************************************************/
 
     // Check managed_file element is enabled.
-    $this->drupalGet('admin/structure/webform/manage/test_element_managed_file/element/add');
+    $this->drupalGet('/admin/structure/webform/manage/test_element_managed_file/element/add');
     $this->assertRaw('>File<');
 
     // Disable managed file element.
@@ -75,11 +75,11 @@ public function testPublicUpload() {
       ->save();
 
     // Check disabled managed_file element remove from add element dialog.
-    $this->drupalGet('admin/structure/webform/manage/test_element_managed_file/element/add');
+    $this->drupalGet('/admin/structure/webform/manage/test_element_managed_file/element/add');
     $this->assertNoRaw('>File<');
 
     // Check disabled managed_file element warning.
-    $this->drupalGet('admin/structure/webform/manage/test_element_managed_file');
+    $this->drupalGet('/admin/structure/webform/manage/test_element_managed_file');
     $this->assertRaw('<em class="placeholder">managed_file_single</em> is a <em class="placeholder">File</em> element, which has been disabled and will not be rendered.');
     $this->assertRaw('<em class="placeholder">managed_file_multiple</em> is a <em class="placeholder">File</em> element, which has been disabled and will not be rendered.');
   }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementManagedFileTest.php b/web/modules/webform/src/Tests/Element/WebformElementManagedFileTest.php
index b25a732bfe48b0d73a2239d238b7c83272e36198..5cdeb6b5918322a139ab3df14eda258e00ac7fba 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementManagedFileTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementManagedFileTest.php
@@ -31,12 +31,39 @@ class WebformElementManagedFileTest extends WebformElementManagedFileTestBase {
     'test_element_managed_file_name',
   ];
 
+  /**
+   * The 'test_element_managed_file' webform.
+   *
+   * @var \Drupal\webform\WebformInterface
+   */
+  protected $webform;
+
+  /**
+   * Admin submission user.
+   *
+   * @var \Drupal\user\Entity\User
+   */
+  protected $adminSubmissionUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->webform = Webform::load('test_element_managed_file');
+
+    $this->adminSubmissionUser = $this->drupalCreateUser([
+      'administer webform submission',
+    ]);
+  }
+
   /**
    * Test single and multiple file upload.
    */
-  public function _testFileUpload() {
+  public function testFileUpload() {
     /* Element rendering */
-    $this->drupalGet('webform/test_element_managed_file');
+    $this->drupalGet('/webform/test_element_managed_file');
 
     // Check single file upload button.
     $this->assertRaw('<label for="edit-managed-file-single-button-upload-button--2" class="button button-action webform-file-button">Choose file</label>');
@@ -51,6 +78,20 @@ public function _testFileUpload() {
 
     $this->checkFileUpload('single', $this->files[0], $this->files[1]);
     $this->checkFileUpload('multiple', $this->files[2], $this->files[3]);
+
+    /* File placeholder */
+
+    // Check placeholder is displayed.
+    $this->drupalGet('/webform/test_element_managed_file');
+    $this->assertRaw('<div class="webform-managed-file-placeholder managed-file-placeholder js-form-wrapper form-wrapper" data-drupal-selector="edit-managed-file-single-placeholder-file-placeholder" id="edit-managed-file-single-placeholder-file-placeholder">This is the single file upload placeholder</div>');
+    $this->assertRaw('<div class="webform-managed-file-placeholder managed-file-placeholder js-form-wrapper form-wrapper" data-drupal-selector="edit-managed-file-multiple-placeholder-file-placeholder" id="edit-managed-file-multiple-placeholder-file-placeholder">This is the multiple file upload placeholder</div>');
+
+    $this->drupalLogin($this->rootUser);
+
+    // Check placeholder is not displayed when files are uploaded.
+    $this->drupalGet('/webform/test_element_managed_file/test');
+    $this->assertNoRaw('<div class="webform-managed-file-placeholder managed-file-placeholder js-form-wrapper form-wrapper" data-drupal-selector="edit-managed-file-single-placeholder-file-placeholder" id="edit-managed-file-single-placeholder-file-placeholder">This is the single file upload placeholder</div>');
+    $this->assertNoRaw('<div class="webform-managed-file-placeholder managed-file-placeholder js-form-wrapper form-wrapper" data-drupal-selector="edit-managed-file-multiple-placeholder-file-placeholder" id="edit-managed-file-multiple-placeholder-file-placeholder">This is the multiple file upload placeholder</div>');
   }
 
   /**
@@ -58,7 +99,7 @@ public function _testFileUpload() {
    *
    * The property #file_name_pattern is tested.
    */
-  protected function testFileRename() {
+  public function testFileRename() {
     $webform = Webform::load('test_element_managed_file_name');
 
     $source_for_filename = $this->randomMachineName();
@@ -97,7 +138,7 @@ protected function testFileRename() {
   /**
    * Test file management.
    */
-  protected function testFileManagement() {
+  public function testFileManagement() {
     $this->drupalLogin($this->rootUser);
 
     $webform = Webform::load('test_element_managed_file');
@@ -164,7 +205,7 @@ protected function testFileManagement() {
   /**
    * Test file upload with disabled results.
    */
-  protected function testFileUploadWithDisabledResults() {
+  public function testFileUploadWithDisabledResults() {
     $this->drupalLogin($this->rootUser);
 
     $webform = Webform::load('test_element_managed_file_dis');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementManagedFileTestBase.php b/web/modules/webform/src/Tests/Element/WebformElementManagedFileTestBase.php
index cf5a608a9af9453365a056e6665cd670c875e6bf..2387214ba9bb13859346e14c7716115453717690 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementManagedFileTestBase.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementManagedFileTestBase.php
@@ -3,7 +3,6 @@
 namespace Drupal\webform\Tests\Element;
 
 use Drupal\file\Entity\File;
-use Drupal\webform\Entity\Webform;
 
 /**
  * Base class for testing webform element managed file handling.
@@ -17,13 +16,6 @@ abstract class WebformElementManagedFileTestBase extends WebformElementTestBase
    */
   public static $modules = ['file', 'webform'];
 
-  /**
-   * Webforms to load.
-   *
-   * @var array
-   */
-  protected static $testWebforms = ['test_element_managed_file'];
-
   /**
    * File usage manager.
    *
@@ -31,13 +23,6 @@ abstract class WebformElementManagedFileTestBase extends WebformElementTestBase
    */
   protected $fileUsage;
 
-  /**
-   * The 'test_element_managed_file' webform.
-   *
-   * @var \Drupal\webform\WebformInterface
-   */
-  protected $webform;
-
   /**
    * An array of plain text test files.
    *
@@ -51,11 +36,7 @@ abstract class WebformElementManagedFileTestBase extends WebformElementTestBase
   public function setUp() {
     parent::setUp();
 
-    // Create users.
-    $this->createUsers();
-
     $this->fileUsage = $this->container->get('file.usage');
-    $this->webform = Webform::load('test_element_managed_file');
     $this->files = $this->drupalGetTestFiles('text');
 
     $this->verbose('<pre>' . print_r($this->files, TRUE) . '</pre>');
@@ -71,10 +52,11 @@ protected function getLastFileId() {
   /**
    * Load an uncached file entity.
    *
-   * @param $fid
+   * @param string $fid
    *   A file id.
    *
    * @return \Drupal\file\FileInterface
+   *   An uncached file object
    */
   protected function fileLoad($fid) {
     \Drupal::entityTypeManager()->getStorage('file')->resetCache();
diff --git a/web/modules/webform/src/Tests/Element/WebformElementMappingTest.php b/web/modules/webform/src/Tests/Element/WebformElementMappingTest.php
index e4086e01dbd61b2af157b7d8ee6490022c41229d..5ab667fa9b6307e7258de33599d112a56b5a0edf 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementMappingTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementMappingTest.php
@@ -20,20 +20,20 @@ class WebformElementMappingTest extends WebformElementTestBase {
    * Test mapping element.
    */
   public function testMappingElement() {
-    $this->drupalGet('webform/test_element_mapping');
+    $this->drupalGet('/webform/test_element_mapping');
 
     // Check default element.
-    $this->assertRaw('<th width="50%">Source &rarr;</th>');
-    $this->assertRaw('<th width="50%">Destination</th>');
+    $this->assertRaw('<th>Source &rarr;</th>');
+    $this->assertRaw('<th>Destination</th>');
     $this->assertRaw('<select data-drupal-selector="edit-webform-mapping-one" id="edit-webform-mapping-one" name="webform_mapping[one]" class="form-select"><option value="" selected="selected">- Select -</option><option value="four">Four</option><option value="five">Five</option><option value="six">Six</option></select>');
 
     // Check custom element.
-    $this->assertRaw('<th width="50%">{Custom source} &raquo;</th>');
-    $this->assertRaw('<th width="50%">{Destination source}</th>');
+    $this->assertRaw('<th>{Custom source} &raquo;</th>');
+    $this->assertRaw('<th>{Destination source}</th>');
     $this->assertRaw('<select data-drupal-selector="edit-webform-mapping-one" id="edit-webform-mapping-one" name="webform_mapping[one]" class="form-select"><option value="" selected="selected">- Select -</option><option value="four">Four</option><option value="five">Five</option><option value="six">Six</option></select>');
 
     // Check custom select other element type.
-    $this->assertRaw('<input data-drupal-selector="edit-webform-mapping-select-other-one-other" type="text" id="edit-webform-mapping-select-other-one-other" name="webform_mapping_select_other[one][other]" value="" size="60" maxlength="128" placeholder="Enter other..." class="form-text" />');
+    $this->assertRaw('<input data-drupal-selector="edit-webform-mapping-select-other-one-other" type="text" id="edit-webform-mapping-select-other-one-other" name="webform_mapping_select_other[one][other]" value="" size="60" maxlength="255" placeholder="Enter other…" class="form-text" />');
 
     // Check custom textfield #size property.
     $this->assertRaw('<input data-drupal-selector="edit-webform-mapping-textfield-one" type="text" id="edit-webform-mapping-textfield-one" name="webform_mapping_textfield[one]" value="" size="10" maxlength="128" class="form-text" />');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementMarkupTest.php b/web/modules/webform/src/Tests/Element/WebformElementMarkupTest.php
index a3e694b51fe1afb47ddd8d8d3835c9ead34c20e3..8f790b9081ebd034161bfea9104172cc006e472f 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementMarkupTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementMarkupTest.php
@@ -9,6 +9,13 @@
  */
 class WebformElementMarkupTest extends WebformElementTestBase {
 
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'webform_test_markup'];
+
   /**
    * Webforms to load.
    *
@@ -20,13 +27,23 @@ class WebformElementMarkupTest extends WebformElementTestBase {
    * Test markup element.
    */
   public function testMarkup() {
-    $this->drupalGet('webform/test_element_markup');
 
+    // Check markup display on form.
+    $this->drupalGet('/webform/test_element_markup');
+    $this->assertRaw('<div id="edit-markup" class="js-form-item form-item js-form-type-webform-markup form-type-webform-markup js-form-item-markup form-item-markup form-no-label">');
     $this->assertRaw('<p>This is normal markup</p>');
     $this->assertRaw('<p>This is only displayed on the form view.</p>');
     $this->assertNoRaw('<p>This is only displayed on the submission view.</p>');
     $this->assertRaw('<p>This is displayed on the both the form and submission view.</p>');
+    $this->assertRaw('<p>This is displayed on the both the form and submission view.</p>');
+
+    // Check markup alter via preprocessing.
+    // @see webform_test_markup_preprocess_webform_html_editor_markup()
+    $this->drupalGet('/webform/test_element_markup');
+    $this->assertNoRaw('<p>Alter this markup.</p>');
+    $this->assertRaw('<p><em>Alter this markup.</em> <strong>This markup was altered.</strong></p>');
 
+    // Check markup display on view.
     $this->drupalPostForm('webform/test_element_markup', [], t('Preview'));
     $this->assertNoRaw('<p>This is normal markup</p>');
     $this->assertNoRaw('<p>This is only displayed on the form view.</p>');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementMediaFileTest.php b/web/modules/webform/src/Tests/Element/WebformElementMediaFileTest.php
index d4df72c49222442e070277417e2b3b7858afafbb..00f5cdb16d54ab52ad502e5046206c81af91dfe7 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementMediaFileTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementMediaFileTest.php
@@ -33,7 +33,7 @@ public function testMediaFileUpload() {
     /* Element render */
 
     // Get test webform.
-    $this->drupalGet('webform/test_element_media_file');
+    $this->drupalGet('/webform/test_element_media_file');
 
     // Check document file.
     $this->assertRaw('<input data-drupal-selector="edit-document-file-upload" type="file" id="edit-document-file-upload" name="files[document_file]" size="22" class="js-form-file form-file" />');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementMessageTest.php b/web/modules/webform/src/Tests/Element/WebformElementMessageTest.php
index 7b65a765326ff639ee446ebdb97dd3bf269dbac3..90e9d75c55a9576f269cea68cc638d596204eb10 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementMessageTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementMessageTest.php
@@ -31,7 +31,7 @@ class WebformElementMessageTest extends WebformElementTestBase {
   public function testMessage() {
     $webform = Webform::load('test_element_message');
 
-    $this->drupalGet('webform/test_element_message');
+    $this->drupalGet('/webform/test_element_message');
 
     // Check basic message.
     $this->assertRaw('<div data-drupal-selector="edit-message-info" class="webform-message js-webform-message js-form-wrapper form-wrapper" id="edit-message-info">');
@@ -54,7 +54,7 @@ public function testMessage() {
 
     // Check that close links are not enabled for 'user' or 'state' storage
     // for anonymous users.
-    $this->drupalGet('webform/test_element_message');
+    $this->drupalGet('/webform/test_element_message');
     $this->assertRaw('href="#close"');
     $this->assertNoRaw('data-message-storage="user"');
     $this->assertNoRaw('data-message-storage="state"');
@@ -63,7 +63,7 @@ public function testMessage() {
     $this->drupalLogin($this->drupalCreateUser());
 
     // Check that close links are enabled.
-    $this->drupalGet('webform/test_element_message');
+    $this->drupalGet('/webform/test_element_message');
     $this->assertNoRaw('href="#close"');
     $this->assertRaw('data-drupal-selector="edit-message-close-storage-user"');
     $this->assertRaw('data-message-storage="user"');
@@ -73,11 +73,11 @@ public function testMessage() {
     $this->assertRaw('data-message-storage="custom"');
 
     // Close message using 'user' storage.
-    $this->drupalGet('webform/test_element_message');
+    $this->drupalGet('/webform/test_element_message');
     $this->clickLink('×', 0);
 
     // Check that 'user' storage message is removed.
-    $this->drupalGet('webform/test_element_message');
+    $this->drupalGet('/webform/test_element_message');
     $this->assertNoRaw('data-drupal-selector="edit-message-close-storage-user"');
     $this->assertNoRaw('data-message-storage="user"');
     $this->assertRaw('data-drupal-selector="edit-message-close-storage-state"');
@@ -86,11 +86,11 @@ public function testMessage() {
     $this->assertRaw('data-message-storage="custom"');
 
     // Close message using 'state' storage.
-    $this->drupalGet('webform/test_element_message');
+    $this->drupalGet('/webform/test_element_message');
     $this->clickLink('×', 0);
 
     // Check that 'state' and 'user' storage message is removed.
-    $this->drupalGet('webform/test_element_message');
+    $this->drupalGet('/webform/test_element_message');
     $this->assertNoRaw('data-drupal-selector="edit-message-close-storage-user"');
     $this->assertNoRaw('data-message-storage="user"');
     $this->assertNoRaw('data-drupal-selector="edit-message-close-storage-state"');
@@ -99,11 +99,11 @@ public function testMessage() {
     $this->assertRaw('data-message-storage="custom"');
 
     // Close message using 'custom' storage.
-    $this->drupalGet('webform/test_element_message');
+    $this->drupalGet('/webform/test_element_message');
     $this->clickLink('×', 0);
 
     // Check that 'state' and 'user' storage message is removed.
-    $this->drupalGet('webform/test_element_message');
+    $this->drupalGet('/webform/test_element_message');
     $this->assertNoRaw('data-drupal-selector="edit-message-close-storage-user"');
     $this->assertNoRaw('data-message-storage="user"');
     $this->assertNoRaw('data-drupal-selector="edit-message-close-storage-state"');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementMoreTest.php b/web/modules/webform/src/Tests/Element/WebformElementMoreTest.php
index 3fa2ddeed134743c27d57a814dbc95669080e877..783d7b21e866f0f0ca97d01f65e16bcc2bb4cc29 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementMoreTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementMoreTest.php
@@ -20,46 +20,42 @@ class WebformElementMoreTest extends WebformElementTestBase {
    * Test element more.
    */
   public function testMore() {
-    $this->drupalGet('webform/test_element_more');
+    $this->drupalGet('/webform/test_element_more');
 
     // Check default more.
-    $this->assertRaw('<div id="edit-more--description" class="description">');
     $this->assertRaw('<div id="edit-more--more" class="js-webform-element-more webform-element-more">');
-    $this->assertRaw('<div class="webform-element-more--link"><a href="#more">More</a></div>');
+    $this->assertRaw('<div class="webform-element-more--link"><a role="button" href="#more">More</a></div>');
 
     // Check more with custom title.
-    $this->assertRaw('<div id="edit-more-title--description" class="description">');
     $this->assertRaw('<div id="edit-more-title--more" class="js-webform-element-more webform-element-more">');
-    $this->assertRaw('<div class="webform-element-more--link"><a href="#more">{Custom more title}</a></div>');
+    $this->assertRaw('<div class="webform-element-more--link"><a role="button" href="#more">{Custom more title}</a></div>');
 
     // Check more with HTML markup.
     $this->assertRaw('<div id="edit-more-html--more" class="js-webform-element-more webform-element-more">');
-    $this->assertRaw('<div class="webform-element-more--content">{This is an example of more with <b>HTML markup</b>}</div>');
+    $this->assertRaw('<div id="edit-more-html--more--content" class="webform-element-more--content">{This is an example of more with <b>HTML markup</b>}</div>');
 
     // Check more with description.
-    $this->assertRaw('<div id="edit-more-title-description--description" class="description">');
-    $this->assertRaw('{This is an example of a description}');
+    $this->assertRaw('<div id="edit-more-title-description--description" class="webform-element-description">{This is an example of a description}</div>');
     $this->assertRaw('<div id="edit-more-title-description--more" class="js-webform-element-more webform-element-more">');
 
     // Check more with hidden description.
-    $this->assertRaw('<div id="edit-more-title-description-hidden--description" class="description">');
-    $this->assertRaw('<div class="visually-hidden">{This is an example of a hidden description}</div>');
+    $this->assertRaw('<div id="edit-more-title-description-hidden--description" class="webform-element-description visually-hidden">{This is an example of a hidden description}</div>');
     $this->assertRaw('<div id="edit-more-title-description-hidden--more" class="js-webform-element-more webform-element-more">');
 
     // Check datetime more.
     $this->assertRaw('<div id="edit-more-datetime--more" class="js-webform-element-more webform-element-more">');
 
     // Check fieldset more.
-    $this->assertRaw('<div id="edit-more-fieldset--description" class="description">{This is a description}');
+    $this->assertRaw('<div id="edit-more-fieldset--description" class="webform-element-description">{This is a description}</div>');
     $this->assertRaw('<div id="edit-more-fieldset--more" class="js-webform-element-more webform-element-more">');
 
     // Check details more.
     $this->assertRaw('<div id="edit-more-details--more" class="js-webform-element-more webform-element-more">');
-    $this->assertRaw('<div class="webform-element-more--link"><a href="#more">More</a></div>');
+    $this->assertRaw('<div class="webform-element-more--link"><a role="button" href="#more">More</a></div>');
 
     // Check tooltip ignored more.
-    $this->assertRaw('<div id="edit-more-tooltip--description" class="description visually-hidden">');
-    $this->assertNoRaw('<div id="edit-more-tooltip--more" class="js-webform-element-more webform-element-more">');
+    $this->assertRaw('<div id="edit-more-tooltip--description" class="webform-element-description visually-hidden">{This is a description}</div>');
+    $this->assertRaw('<div id="edit-more-tooltip--more" class="js-webform-element-more webform-element-more">');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementMultipleTest.php b/web/modules/webform/src/Tests/Element/WebformElementMultipleTest.php
index cd6b8cac6b706fe94c3cab7de18e76a6b909ff38..0891ff8c757a3667860db1f79fd1bd7c2a96c28e 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementMultipleTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementMultipleTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\webform\Tests\Element;
 
+use Drupal\webform\Entity\Webform;
+
 /**
  * Tests for webform element multiple.
  *
@@ -25,6 +27,8 @@ public function testMultiple() {
     // Processing.
     /**************************************************************************/
 
+    $webform = Webform::load('test_element_multiple');
+
     // Check processing for all elements.
     $this->drupalPostForm('webform/test_element_multiple', [], t('Submit'));
     $this->assertRaw("webform_multiple_default:
@@ -39,6 +43,14 @@ public function testMultiple() {
   - One
   - Two
   - Three
+webform_multiple_no_add_more:
+  - One
+  - Two
+  - Three
+webform_multiple_custom_label:
+  - One
+  - Two
+  - Three
 webform_multiple_email_five:
   - example@example.com
   - test@test.com
@@ -91,19 +103,20 @@ public function testMultiple() {
     description: 'This is the number 1.'
   - value: two
     text: Two
-    description: 'This is the number 2.'");
+    description: 'This is the number 2.'
+webform_multiple_no_items: {  }");
 
     /**************************************************************************/
     // Rendering.
     /**************************************************************************/
 
-    $this->drupalGet('webform/test_element_multiple');
+    $this->drupalGet('/webform/test_element_multiple');
 
     // Check first tr.
     $this->assertRaw('<tr class="draggable odd" data-drupal-selector="edit-webform-multiple-default-items-0">');
     $this->assertRaw('<td><div class="js-form-item form-item js-form-type-textfield form-type-textfield js-form-item-webform-multiple-default-items-0--item- form-item-webform-multiple-default-items-0--item- form-no-label">');
     $this->assertRaw('<label for="edit-webform-multiple-default-items-0-item-" class="visually-hidden">Item value</label>');
-    $this->assertRaw('<input data-drupal-selector="edit-webform-multiple-default-items-0-item-" type="text" id="edit-webform-multiple-default-items-0-item-" name="webform_multiple_default[items][0][_item_]" value="One" size="60" maxlength="128" placeholder="Enter value" class="form-text" />');
+    $this->assertRaw('<input data-drupal-selector="edit-webform-multiple-default-items-0-item-" type="text" id="edit-webform-multiple-default-items-0-item-" name="webform_multiple_default[items][0][_item_]" value="One" size="60" maxlength="128" placeholder="Enter value…" class="form-text" />');
     $this->assertRaw('<td class="webform-multiple-table--weight"><div class="webform-multiple-table--weight js-form-item form-item js-form-type-number form-type-number js-form-item-webform-multiple-default-items-0-weight form-item-webform-multiple-default-items-0-weight form-no-label">');
     $this->assertRaw('<label for="edit-webform-multiple-default-items-0-weight" class="visually-hidden">Item weight</label>');
     $this->assertRaw('<input class="webform-multiple-sort-weight form-number" data-drupal-selector="edit-webform-multiple-default-items-0-weight" type="number" id="edit-webform-multiple-default-items-0-weight" name="webform_multiple_default[items][0][weight]" value="0" step="1" size="10" />');
@@ -114,9 +127,20 @@ public function testMultiple() {
     $this->assertNoRaw('<tr class="draggable odd" data-drupal-selector="edit-webform-multiple-no-sorting-items-0">');
     $this->assertRaw('<tr data-drupal-selector="edit-webform-multiple-no-sorting-items-0" class="odd">');
 
+    // Check that add more is removed.
+    $this->assertFieldByName('webform_multiple_no_operations[add][more_items]', '1');
+    $this->assertNoFieldByName('webform_multiple_no_add_more[add][more_items]', '1');
+
+    // Check custom labels.
+    $this->assertRaw('<input data-drupal-selector="edit-webform-multiple-custom-label-add-submit" formnovalidate="formnovalidate" type="submit" id="edit-webform-multiple-custom-label-add-submit" name="webform_multiple_custom_label_table_add" value="{add_more_button_label}" class="button js-form-submit form-submit" />');
+    $this->assertRaw('<span class="field-suffix">{add_more_input_label}</span>');
+
     // Check that operations is disabled.
     $this->assertNoRaw('data-drupal-selector="edit-webform-multiple-no-operations-items-0-operations-remove"');
 
+    // Check no items message.
+    $this->assertRaw('No items entered. Please add items below.');
+
     /**************************************************************************/
     // Validation.
     /**************************************************************************/
@@ -175,6 +199,19 @@ public function testMultiple() {
     $this->assertFieldByName('webform_multiple_default[items][1][_item_]', 'Three');
     $this->assertFieldByName('webform_multiple_default[items][2][_item_]', 'Four');
 
+    // Add one options to 'webform_multiple_no_items'.
+    $this->drupalPostAjaxForm(NULL, $edit, 'webform_multiple_no_items_table_add');
+    $this->assertNoRaw('No items entered. Please add items below.');
+    $this->assertFieldByName('webform_multiple_no_items[items][0][_item_]');
+
+    // Check no items message is never displayed when #required.
+    $webform->setElementProperties('webform_multiple_no_items', ['#type' => 'webform_multiple', '#title' => 'webform_multiple_no_items', '#required' => TRUE]);
+    $webform->save();
+    $this->drupalGet('/webform/test_element_multiple');
+    $this->assertNoRaw('No items entered. Please add items below.');
+    $this->drupalPostAjaxForm(NULL, $edit, 'webform_multiple_default_table_remove_0');
+    $this->assertNoRaw('No items entered. Please add items below.');
+
     /**************************************************************************/
     // Property (#multiple).
     /**************************************************************************/
diff --git a/web/modules/webform/src/Tests/Element/WebformElementOptionsTest.php b/web/modules/webform/src/Tests/Element/WebformElementOptionsTest.php
index 2a613839daa64f4c8b771efab733914c6c9f7fcb..d3169acce8fd871c73a3bcebfa79084a7af84063 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementOptionsTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementOptionsTest.php
@@ -21,9 +21,9 @@ class WebformElementOptionsTest extends WebformElementTestBase {
    */
   public function testElementOptions() {
     // Check options maxlength.
-    $this->drupalGet('webform/test_element_options');
-    $this->assertRaw('<input class="js-webform-options-value form-text" data-drupal-selector="edit-webform-options-maxlength-options-items-0-value" type="text" id="edit-webform-options-maxlength-options-items-0-value" name="webform_options_maxlength[options][items][0][value]" value="one" size="60" maxlength="20" placeholder="Enter value" />');
-    $this->assertRaw('<input data-drupal-selector="edit-webform-options-maxlength-options-items-0-text" type="text" id="edit-webform-options-maxlength-options-items-0-text" name="webform_options_maxlength[options][items][0][text]" value="One" size="60" maxlength="20" placeholder="Enter text" class="form-text" />');
+    $this->drupalGet('/webform/test_element_options');
+    $this->assertRaw('<input class="js-webform-options-sync form-text" data-drupal-selector="edit-webform-options-maxlength-options-items-0-value" type="text" id="edit-webform-options-maxlength-options-items-0-value" name="webform_options_maxlength[options][items][0][value]" value="one" size="60" maxlength="20" placeholder="Enter value…" />');
+    $this->assertRaw('<input data-drupal-selector="edit-webform-options-maxlength-options-items-0-text" type="text" id="edit-webform-options-maxlength-options-items-0-text" name="webform_options_maxlength[options][items][0][text]" value="One" size="60" maxlength="20" placeholder="Enter text…" class="form-text" />');
 
     // Check default value handling.
     $this->drupalPostForm('webform/test_element_options', [], t('Submit'));
@@ -52,6 +52,14 @@ public function testElementOptions() {
     // Check default value handling.
     $this->drupalPostForm('webform/test_element_options', ['webform_element_options_custom[options]' => 'yes_no'], t('Submit'));
     $this->assertRaw("webform_element_options_custom: yes_no");
+
+    // Check unique option value validation.
+    $edit = [
+      'webform_options[options][items][0][value]' => 'test',
+      'webform_options[options][items][1][value]' => 'test',
+    ];
+    $this->drupalPostForm('webform/test_element_options', $edit, t('Submit'));
+    $this->assertRaw('The <em class="placeholder">Option value</em> \'test\' is already in use. It must be unique.');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementOtherTest.php b/web/modules/webform/src/Tests/Element/WebformElementOtherTest.php
index 0ce872f3e44861c7321436ad717c3afece92c75a..2f0fdd049a3894d4c478e8b48c3f5004af711063 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementOtherTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementOtherTest.php
@@ -22,30 +22,31 @@ class WebformElementOtherTest extends WebformElementTestBase {
    * Tests options with other elements.
    */
   public function testBuildingOtherElements() {
-    $this->drupalGet('webform/test_element_other');
+    $this->drupalGet('/webform/test_element_other');
 
     /**************************************************************************/
     // select_other
     /**************************************************************************/
 
     // Check basic select_other.
+    $this->assertRaw('<fieldset data-drupal-selector="edit-select-other-basic" class="js-webform-select-other webform-select-other webform-select-other--wrapper fieldgroup form-composite webform-composite-visible-title js-webform-type-webform-select-other webform-type-webform-select-other js-form-item form-item js-form-wrapper form-wrapper" id="edit-select-other-basic">');
+    $this->assertRaw('<span class="fieldset-legend">Select other basic</span>');
     $this->assertRaw('<select data-drupal-selector="edit-select-other-basic-select" id="edit-select-other-basic-select" name="select_other_basic[select]" class="form-select">');
-    $this->assertRaw('<input data-drupal-selector="edit-select-other-basic-other" type="text" id="edit-select-other-basic-other" name="select_other_basic[other]" value="Four" size="60" maxlength="128" placeholder="Enter other..." class="form-text" />');
-    $this->assertRaw('<option value="_other_" selected="selected">Other...</option>');
+    $this->assertRaw('<input data-drupal-selector="edit-select-other-basic-other" type="text" id="edit-select-other-basic-other" name="select_other_basic[other]" value="Four" size="60" maxlength="255" placeholder="Enter other…" class="form-text" />');
+    $this->assertRaw('<option value="_other_" selected="selected">Other…</option>');
 
     // Check advanced select_other w/ custom label.
     $this->assertRaw('<span class="fieldset-legend js-form-required form-required">Select other advanced</span>');
     $this->assertRaw('<select data-drupal-selector="edit-select-other-advanced-select" id="edit-select-other-advanced-select" name="select_other_advanced[select]" class="form-select required" required="required" aria-required="true">');
     $this->assertRaw('<option value="_other_" selected="selected">Is there another option you wish to enter?</option>');
     $this->assertRaw('<label for="edit-select-other-advanced-other">Other</label>');
-    $this->assertRaw('<input data-drupal-selector="edit-select-other-advanced-other" aria-describedby="edit-select-other-advanced-other--description" type="text" id="edit-select-other-advanced-other" name="select_other_advanced[other]" value="Four" size="20" maxlength="20" placeholder="What is this other option" class="form-text" />');
-    $this->assertRaw('<div id="edit-select-other-advanced-other--description" class="description">');
-    $this->assertRaw('Other select description');
+    $this->assertRaw('<input data-counter-type="character" data-counter-minimum="4" data-counter-maximum="10" class="js-webform-counter webform-counter form-text" data-drupal-selector="edit-select-other-advanced-other" aria-describedby="edit-select-other-advanced-other--description" type="text" id="edit-select-other-advanced-other" name="select_other_advanced[other]" value="Four" size="20" maxlength="10" placeholder="What is this other option" />');
+    $this->assertRaw('<div id="edit-select-other-advanced-other--description" class="webform-element-description">Other select description</div>');
 
     // Check multiple select_other.
     $this->assertRaw('<span class="fieldset-legend">Select other multiple</span>');
     $this->assertRaw('<select data-drupal-selector="edit-select-other-multiple-select" multiple="multiple" name="select_other_multiple[select][]" id="edit-select-other-multiple-select" class="form-select">');
-    $this->assertRaw('<input data-drupal-selector="edit-select-other-multiple-other" type="text" id="edit-select-other-multiple-other" name="select_other_multiple[other]" value="Four" size="60" maxlength="128" placeholder="Enter other..." class="form-text" />');
+    $this->assertRaw('<input data-drupal-selector="edit-select-other-multiple-other" type="text" id="edit-select-other-multiple-other" name="select_other_multiple[other]" value="Four" size="60" maxlength="255" placeholder="Enter other…" class="form-text" />');
 
     /**************************************************************************/
     // checkboxes_other
@@ -54,15 +55,15 @@ public function testBuildingOtherElements() {
     // Check basic checkboxes.
     $this->assertRaw('<span class="fieldset-legend">Checkboxes other basic</span>');
     $this->assertRaw('<input data-drupal-selector="edit-checkboxes-other-basic-checkboxes-other-" type="checkbox" id="edit-checkboxes-other-basic-checkboxes-other-" name="checkboxes_other_basic[checkboxes][_other_]" value="_other_" checked="checked" class="form-checkbox" />');
-    $this->assertRaw('<label for="edit-checkboxes-other-basic-checkboxes-other-" class="option">Other...</label>');
-    $this->assertRaw('<input data-drupal-selector="edit-checkboxes-other-basic-other" type="text" id="edit-checkboxes-other-basic-other" name="checkboxes_other_basic[other]" value="Four" size="60" maxlength="128" placeholder="Enter other..." class="form-text" />');
+    $this->assertRaw('<label for="edit-checkboxes-other-basic-checkboxes-other-" class="option">Other…</label>');
+    $this->assertRaw('<input data-drupal-selector="edit-checkboxes-other-basic-other" type="text" id="edit-checkboxes-other-basic-other" name="checkboxes_other_basic[other]" value="Four" size="60" maxlength="255" placeholder="Enter other…" class="form-text" />');
 
     // Check advanced checkboxes.
     $this->assertRaw('<div id="edit-checkboxes-other-advanced-checkboxes" class="js-webform-checkboxes webform-options-display-two-columns form-checkboxes">');
     $this->assertRaw('<span class="fieldset-legend js-form-required form-required">Checkboxes other advanced</span>');
-    $this->assertRaw('<input data-drupal-selector="edit-checkboxes-other-advanced-other" aria-describedby="edit-checkboxes-other-advanced-other--description" type="text" id="edit-checkboxes-other-advanced-other" name="checkboxes_other_advanced[other]" value="Four" size="60" maxlength="128" placeholder="What is this other option" class="form-text" />');
-    $this->assertRaw('<div id="edit-checkboxes-other-advanced-other--description" class="description">');
-    $this->assertRaw('Other checkbox description');
+    $this->assertRaw('<input data-drupal-selector="edit-checkboxes-other-advanced-other" aria-describedby="edit-checkboxes-other-advanced-other--description" type="text" id="edit-checkboxes-other-advanced-other" name="checkboxes_other_advanced[other]" value="Four" size="60" maxlength="255" placeholder="What is this other option" class="form-text" />');
+    $this->assertRaw('<div id="edit-checkboxes-other-advanced-other--description" class="webform-element-description">Other checkbox description</div>');
+    $this->assertRaw('<label for="edit-checkboxes-other-advanced-checkboxes-one" class="option">One<span class="webform-element-help"');
 
     /**************************************************************************/
     // radios_other
@@ -71,15 +72,15 @@ public function testBuildingOtherElements() {
     // Check basic radios_other.
     $this->assertRaw('<span class="fieldset-legend">Radios other basic</span>');
     $this->assertRaw('<input data-drupal-selector="edit-radios-other-basic-radios-other-" type="radio" id="edit-radios-other-basic-radios-other-" name="radios_other_basic[radios]" value="_other_" checked="checked" class="form-radio" />');
-    $this->assertRaw('<label for="edit-radios-other-basic-radios-other-" class="option">Other...</label>');
-    $this->assertRaw('<input data-drupal-selector="edit-radios-other-basic-other" type="text" id="edit-radios-other-basic-other" name="radios_other_basic[other]" value="Four" size="60" maxlength="128" placeholder="Enter other..." class="form-text" />');
+    $this->assertRaw('<label for="edit-radios-other-basic-radios-other-" class="option">Other…</label>');
+    $this->assertRaw('<input data-drupal-selector="edit-radios-other-basic-other" type="text" id="edit-radios-other-basic-other" name="radios_other_basic[other]" value="Four" size="60" maxlength="255" placeholder="Enter other…" class="form-text" />');
 
     // Check advanced radios_other w/ custom label.
     $this->assertRaw('<span class="fieldset-legend js-form-required form-required">Radios other advanced</span>');
     $this->assertRaw('<input data-drupal-selector="edit-radios-other-advanced-radios-other-" type="radio" id="edit-radios-other-advanced-radios-other-" name="radios_other_advanced[radios]" value="_other_" checked="checked" class="form-radio" />');
-    $this->assertRaw('<input data-drupal-selector="edit-radios-other-advanced-other" aria-describedby="edit-radios-other-advanced-other--description" type="text" id="edit-radios-other-advanced-other" name="radios_other_advanced[other]" value="Four" size="60" maxlength="128" placeholder="What is this other option" class="form-text" />');
-    $this->assertRaw('<div id="edit-radios-other-advanced-other--description" class="description">');
-    $this->assertRaw('Other radio description');
+    $this->assertRaw('<input data-drupal-selector="edit-radios-other-advanced-other" aria-describedby="edit-radios-other-advanced-other--description" type="text" id="edit-radios-other-advanced-other" name="radios_other_advanced[other]" value="Four" size="60" maxlength="255" placeholder="What is this other option" class="form-text" />');
+    $this->assertRaw('<div id="edit-radios-other-advanced-other--description" class="webform-element-description">Other radio description</div>');
+    $this->assertRaw('<label for="edit-radios-other-advanced-radios-one" class="option">One<span class="webform-element-help"');
 
     /**************************************************************************/
     // buttons_other
@@ -89,14 +90,23 @@ public function testBuildingOtherElements() {
     $this->assertRaw('<span class="fieldset-legend">Buttons other basic</span>');
     $this->assertRaw('<input data-drupal-selector="edit-buttons-other-basic-buttons-one" type="radio" id="edit-buttons-other-basic-buttons-one" name="buttons_other_basic[buttons]" value="One" class="form-radio" />');
     $this->assertRaw('<label for="edit-buttons-other-basic-buttons-one" class="option">One</label>');
-    $this->assertRaw('<input data-drupal-selector="edit-buttons-other-basic-other" type="text" id="edit-buttons-other-basic-other" name="buttons_other_basic[other]" value="Four" size="60" maxlength="128" placeholder="Enter other..." class="form-text" />');
+    $this->assertRaw('<input data-drupal-selector="edit-buttons-other-basic-other" type="text" id="edit-buttons-other-basic-other" name="buttons_other_basic[other]" value="Four" size="60" maxlength="255" placeholder="Enter other…" class="form-text" />');
 
     // Check advanced buttons_other w/ custom label.
     $this->assertRaw('<span class="fieldset-legend js-form-required form-required">Buttons other advanced</span>');
     $this->assertRaw('<input data-drupal-selector="edit-buttons-other-advanced-buttons-one" type="radio" id="edit-buttons-other-advanced-buttons-one" name="buttons_other_advanced[buttons]" value="One" class="form-radio" />');
-    $this->assertRaw('<input data-drupal-selector="edit-buttons-other-advanced-other" aria-describedby="edit-buttons-other-advanced-other--description" type="text" id="edit-buttons-other-advanced-other" name="buttons_other_advanced[other]" value="Four" size="60" maxlength="128" placeholder="What is this other option" class="form-text" />');
-    $this->assertRaw('<div id="edit-buttons-other-advanced-other--description" class="description">');
-    $this->assertRaw('Other button description');
+    $this->assertRaw('<input data-drupal-selector="edit-buttons-other-advanced-other" aria-describedby="edit-buttons-other-advanced-other--description" type="text" id="edit-buttons-other-advanced-other" name="buttons_other_advanced[other]" value="Four" size="60" maxlength="255" placeholder="What is this other option" class="form-text" />');
+    $this->assertRaw('<div id="edit-buttons-other-advanced-other--description" class="webform-element-description">Other button description</div>');
+
+    /**************************************************************************/
+    // wrapper_type
+    /**************************************************************************/
+
+    // Check form_item wrapper type.
+    $this->assertRaw('<div class="js-webform-select-other webform-select-other js-form-item form-item js-form-type-webform-select-other form-type-webform-select-other js-form-item-wrapper-other-form-element form-item-wrapper-other-form-element" id="edit-wrapper-other-form-element">');
+
+    // Check container wrapper type.
+    $this->assertRaw('<div data-drupal-selector="edit-wrapper-other-container" class="js-webform-select-other webform-select-other webform-select-other--wrapper fieldgroup form-composite js-form-wrapper form-wrapper" id="edit-wrapper-other-container">');
   }
 
   /**
@@ -133,6 +143,14 @@ public function testProcessingOtherElements() {
     $this->drupalPostForm('webform/test_element_other', $edit, t('Submit'));
     $this->assertRaw('Select other advanced field is required.');
 
+    // Check select other processing w/ other min/max character validation.
+    $edit = [
+      'select_other_advanced[select]' => '_other_',
+      'select_other_advanced[other]' => 'X',
+    ];
+    $this->drupalPostForm('webform/test_element_other', $edit, t('Submit'));
+    $this->assertRaw('Other must be longer than <em class="placeholder">4</em> characters but is currently <em class="placeholder">1</em> characters long.');
+
     // Check select other processing w/ other.
     $edit = [
       'select_other_advanced[select]' => '_other_',
diff --git a/web/modules/webform/src/Tests/Element/WebformElementPatternTest.php b/web/modules/webform/src/Tests/Element/WebformElementPatternTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..4a5f62ad1113518ad901a6dfbeb1bb85f79807a5
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementPatternTest.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+/**
+ * Tests for webform pattern validation.
+ *
+ * @group Webform
+ */
+class WebformElementPatternTest extends WebformElementTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_pattern'];
+
+  /**
+   * Tests pattern validation.
+   */
+  public function testPattern() {
+    // Check rendering.
+    $this->drupalGet('/webform/test_element_pattern');
+    $this->assertRaw('<input pattern="Hello" data-drupal-selector="edit-pattern" aria-describedby="edit-pattern--description" type="text" id="edit-pattern" name="pattern" value="" size="60" maxlength="255" class="form-text" />');
+    $this->assertRaw('<input pattern="Hello" data-webform-pattern-error="You did not enter &#039;Hello&#039;" data-drupal-selector="edit-pattern-error" aria-describedby="edit-pattern-error--description" type="text" id="edit-pattern-error" name="pattern_error" value="" size="60" maxlength="255" class="form-text" />');
+    $this->assertRaw('<input pattern="\u2E8F" data-drupal-selector="edit-pattern-unicode" aria-describedby="edit-pattern-unicode--description" type="text" id="edit-pattern-unicode" name="pattern_unicode" value="" size="60" maxlength="255" class="form-text" />');
+
+    // Check validation.
+    $edit = [
+      'pattern' => 'GoodBye',
+      'pattern_error' => 'GoodBye',
+      'pattern_unicode' => 'Unicode',
+    ];
+    $this->drupalPostForm('webform/test_element_pattern', $edit, t('Submit'));
+    $this->assertRaw('<li class="messages__item"><em class="placeholder">pattern</em> field is not in the right format.</li>');
+    $this->assertRaw('<li class="messages__item">You did not enter &#039;Hello&#039;</li>');
+    $this->assertRaw('<li class="messages__item"><em class="placeholder">pattern_unicode</em> field is not in the right format.</li>');
+
+    // Check validation.
+    $edit = [
+      'pattern' => 'Hello',
+      'pattern_error' => 'Hello',
+      'pattern_unicode' => '⺏',
+    ];
+    $this->drupalPostForm('webform/test_element_pattern', $edit, t('Submit'));
+    $this->assertNoRaw('<li class="messages__item"><em class="placeholder">pattern</em> field is not in the right format.</li>');
+    $this->assertNoRaw('<li class="messages__item">You did not enter &#039;Hello&#039;</li>');
+    $this->assertNoRaw('<li class="messages__item"><em class="placeholder">pattern_unicode</em> field is not in the right format.</li>');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementPluginTest.php b/web/modules/webform/src/Tests/Element/WebformElementPluginTest.php
index ec16bc7050ba089d577a2d87e5e5d80adb60b185..436b3eaf2c0606c244be1d46266840fcc7032192 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementPluginTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementPluginTest.php
@@ -38,7 +38,7 @@ public function testElementPlugin() {
 
     // Check that managed_file and webform_term-select are not available when
     // dependent modules are not installed.
-    $this->drupalGet('admin/structure/webform/plugins/elements');
+    $this->drupalGet('/admin/reports/webform-plugins/elements');
     $this->assertNoRaw('<td><div class="webform-form-filter-text-source">managed_file</div></td>');
     $this->assertNoRaw('<td><div class="webform-form-filter-text-source">webform_term_select</div></td>');
 
@@ -47,7 +47,7 @@ public function testElementPlugin() {
 
     // Check that managed_file and webform_term-select are available when
     // dependent modules are installed.
-    $this->drupalGet('admin/structure/webform/plugins/elements');
+    $this->drupalGet('/admin/reports/webform-plugins/elements');
     $this->assertRaw('<td><div class="webform-form-filter-text-source">managed_file</div></td>');
     $this->assertRaw('<td><div class="webform-form-filter-text-source">webform_term_select</div></td>');
 
@@ -59,7 +59,7 @@ public function testElementPlugin() {
     $webform_plugin_test = Webform::load('test_element_plugin');
 
     // Check prepare and setDefaultValue().
-    $this->drupalGet('webform/test_element_plugin');
+    $this->drupalGet('/webform/test_element_plugin');
     $this->assertRaw('Invoked: Drupal\webform_test_element\Plugin\WebformElement\WebformTestElement:preCreate');
     $this->assertRaw('Invoked: Drupal\webform_test_element\Plugin\WebformElement\WebformTestElement:postCreate');
     $this->assertRaw('Invoked: Drupal\webform_test_element\Plugin\WebformElement\WebformTestElement:prepare');
@@ -100,7 +100,7 @@ public function testElementPlugin() {
     $this->drupalPostForm('/admin/structure/webform/manage/test_element_plugin/submission/' . $sid . '/delete', [], t('Delete'));
     $this->assertRaw('Invoked: Drupal\webform_test_element\Plugin\WebformElement\WebformTestElement:preDelete');
     $this->assertRaw('Invoked: Drupal\webform_test_element\Plugin\WebformElement\WebformTestElement:postDelete');
-    $this->assertRaw('Test: Element: Test (plugin): Submission #' . $webform_submission->serial() . ' has been deleted.');
+    $this->assertRaw('<em class="placeholder">Test: Element: Test (plugin): Submission #' . $webform_submission->serial() . '</em> has been deleted.');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementPrepopulateTest.php b/web/modules/webform/src/Tests/Element/WebformElementPrepopulateTest.php
index f2a4fabc2020c81cae0327d0a0081900cc4fb7fa..60fa1f3f3ee72d291e0f51653a8788eac4d678f4 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementPrepopulateTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementPrepopulateTest.php
@@ -35,17 +35,17 @@ public function testElementPrepopulate() {
     $files = $this->drupalGetTestFiles('text');
 
     // Check prepopulation of an element.
-    $this->drupalGet('webform/test_element_prepopulate');
+    $this->drupalGet('/webform/test_element_prepopulate');
     $this->assertFieldByName('textfield', '');
     $this->assertFieldByName('textfield_prepopulate', '');
     $this->assertFieldByName('files[managed_file_prepopulate]', '');
 
     // Check 'textfield' can not be prepopulated.
-    $this->drupalGet('webform/test_element_prepopulate', ['query' => ['textfield' => 'value']]);
+    $this->drupalGet('/webform/test_element_prepopulate', ['query' => ['textfield' => 'value']]);
     $this->assertNoFieldByName('textfield', 'value');
 
     // Check 'textfield_prepopulate' can be prepopulated.
-    $this->drupalGet('webform/test_element_prepopulate', ['query' => ['textfield_prepopulate' => 'value']]);
+    $this->drupalGet('/webform/test_element_prepopulate', ['query' => ['textfield_prepopulate' => 'value']]);
     $this->assertFieldByName('textfield_prepopulate', 'value');
 
     // Check 'managed_file_prepopulate' can not be prepopulated.
@@ -57,7 +57,7 @@ public function testElementPrepopulate() {
     $sid = $this->postSubmission($webform, $edit);
     $webform_submission = WebformSubmission::load($sid);
     $fid = $webform_submission->getElementData('managed_file_prepopulate');
-    $this->drupalGet('webform/test_element_prepopulate', ['query' => ['managed_file_prepopulate' => $fid]]);
+    $this->drupalGet('/webform/test_element_prepopulate', ['query' => ['managed_file_prepopulate' => $fid]]);
     $this->assertFieldByName('files[managed_file_prepopulate]', '');
   }
 
diff --git a/web/modules/webform/src/Tests/Element/WebformElementPrivateTest.php b/web/modules/webform/src/Tests/Element/WebformElementPrivateTest.php
index 21d50960ba731f64a352a3f9abaea8e2fad2b220..03a86431a60a44beb37e8c76d9c6f8f354e52166 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementPrivateTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementPrivateTest.php
@@ -18,34 +18,29 @@ class WebformElementPrivateTest extends WebformElementTestBase {
    */
   protected static $testWebforms = ['test_element_private'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
   /**
    * Test element access.
    */
   public function testElementAccess() {
+    $normal_user = $this->drupalCreateUser();
+
     $webform = Webform::load('test_element_private');
 
+    /**************************************************************************/
+
+    $this->drupalLogin($normal_user);
+
     // Create a webform submission.
-    $this->drupalLogin($this->normalUser);
     $this->postSubmission($webform);
 
     // Check element with #private property hidden for normal user.
-    $this->drupalLogin($this->normalUser);
-    $this->drupalGet('webform/test_element_private');
+    $this->drupalGet('/webform/test_element_private');
     $this->assertNoFieldByName('private', '');
 
-    // Check element with #private property visible for admin user.
     $this->drupalLogin($this->rootUser);
-    $this->drupalGet('webform/test_element_private');
+
+    // Check element with #private property visible for admin user.
+    $this->drupalGet('/webform/test_element_private');
     $this->assertFieldByName('private', '');
   }
 
diff --git a/web/modules/webform/src/Tests/Element/WebformElementRadiosTest.php b/web/modules/webform/src/Tests/Element/WebformElementRadiosTest.php
index 3c66982c6bc47db6fe104af2494feff31ee19050..99cccbbc88d33328601d86307d669e3715de3595 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementRadiosTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementRadiosTest.php
@@ -3,7 +3,7 @@
 namespace Drupal\webform\Tests\Element;
 
 /**
- * Tests for webform element radioss.
+ * Tests for webform element radios.
  *
  * @group Webform
  */
@@ -22,16 +22,16 @@ class WebformElementRadiosTest extends WebformElementTestBase {
   public function testElementRadios() {
     $this->drupalLogin($this->rootUser);
 
-    $this->drupalGet('webform/test_element_radios');
+    $this->drupalGet('/webform/test_element_radios');
 
     // Check radios with description display.
     $this->assertRaw('<input data-drupal-selector="edit-radios-description-one" aria-describedby="edit-radios-description-one--description" type="radio" id="edit-radios-description-one" name="radios_description" value="one" class="form-radio" />');
     $this->assertRaw('<label for="edit-radios-description-one" class="option">One</label>');
-    $this->assertRaw('<div id="edit-radios-description-one--description" class="description">');
+    $this->assertRaw('<div id="edit-radios-description-one--description" class="webform-element-description">This is a description</div>');
 
     // Check radios with help text display.
     $this->assertRaw('<input data-drupal-selector="edit-radios-help-one" type="radio" id="edit-radios-help-one" name="radios_help" value="one" class="form-radio" />');
-    $this->assertRaw('<label for="edit-radios-help-one" class="option">One<a href="#help" title="This is a description" data-webform-help="This is a description" class="webform-element-help">?</a>');
+    $this->assertRaw('<label for="edit-radios-help-one" class="option">One<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;One&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;This is a description&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
 
     // Check radios results does not include description.
     $edit = [
diff --git a/web/modules/webform/src/Tests/Element/WebformElementRangeTest.php b/web/modules/webform/src/Tests/Element/WebformElementRangeTest.php
index 242a9e91f27178d9a1dd4961066392f384cd061d..f89fba073927be98d1dc079abbfca6610e8f3b57 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementRangeTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementRangeTest.php
@@ -20,7 +20,7 @@ class WebformElementRangeTest extends WebformElementTestBase {
    * Test range element.
    */
   public function testRating() {
-    $this->drupalGet('webform/test_element_range');
+    $this->drupalGet('/webform/test_element_range');
 
     // Check basic range element.
     $this->assertRaw('<input data-drupal-selector="edit-range" type="range" id="edit-range" name="range" value="" step="1" min="0" max="100" class="form-range" />');
@@ -39,9 +39,13 @@ public function testRating() {
 
     // Check output left range element.
     $this->assertRaw('<span class="field-prefix"><div class="js-form-item form-item js-form-type-number form-type-number js-form-item-range-output-left__output form-item-range-output-left__output form-no-label">');
+    $this->assertRaw('<label for="range_output_left__output" class="visually-hidden">range_output_left</label>');
+    $this->assertRaw('<input style="background-color: yellow;width:6em" type="number" id="range_output_left__output" step="100" min="0" max="10000" class="form-number" />');
 
     // Check output right range element.
     $this->assertRaw('<span class="field-suffix"><span class="webform-range-output-delimiter"></span><div class="js-form-item form-item js-form-type-number form-type-number js-form-item-range-output-disabled__output form-item-range-output-disabled__output form-no-label form-disabled">');
+    $this->assertRaw('<label for="range_output_right__output" class="visually-hidden">range_output_right</label>');
+    $this->assertRaw('<input style="width:4em" type="number" id="range_output_right__output" step="1" min="0" max="100" class="form-number" />');
 
     // Check processing.
     $this->drupalPostForm('webform/test_element_range', [], t('Submit'));
diff --git a/web/modules/webform/src/Tests/Element/WebformElementRatingTest.php b/web/modules/webform/src/Tests/Element/WebformElementRatingTest.php
index b7c161e3cda7f291102a3c962ee511eb8fce437f..4bf0d17604da765707b231de89a21869e332bee1 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementRatingTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementRatingTest.php
@@ -20,24 +20,37 @@ class WebformElementRatingTest extends WebformElementTestBase {
    * Test rating element.
    */
   public function testRating() {
-    $this->drupalGet('webform/test_element_rating');
+    $this->drupalGet('/webform/test_element_rating');
 
     // Check basic rating display.
-    $this->assertRaw('<label for="edit-rating-basic">Rating basic</label>');
+    $this->assertRaw('<label for="edit-rating-basic">rating_basic</label>');
     $this->assertRaw('<input data-drupal-selector="edit-rating-basic" type="range" id="edit-rating-basic" name="rating_basic" value="0" step="1" min="0" max="5" class="form-webform-rating" />');
-    $this->assertRaw('<div class="rateit svg rateit-medium" data-rateit-min="0" data-rateit-max="5" data-rateit-step="1" data-rateit-resetable="false" data-rateit-readonly="false" data-rateit-backingfld="#edit-rating-basic" data-rateit-value="" data-rateit-starheight="24" data-rateit-starwidth="24">');
+    $this->assertRaw('<div class="rateit svg rateit-medium" data-rateit-min="0" data-rateit-max="5" data-rateit-step="1" data-rateit-resetable="false" data-rateit-readonly="false" data-rateit-backingfld="[data-drupal-selector=&quot;edit-rating-basic&quot;]" data-rateit-value="" data-rateit-starheight="24" data-rateit-starwidth="24">');
 
     // Check advanced rating display.
-    $this->assertRaw('<label for="edit-rating-advanced">Rating advanced</label>');
+    $this->assertRaw('<label for="edit-rating-advanced">rating_advanced</label>');
     $this->assertRaw('<input data-drupal-selector="edit-rating-advanced" type="range" id="edit-rating-advanced" name="rating_advanced" value="0" step="0.1" min="0" max="10" class="form-webform-rating" />');
-    $this->assertRaw('<div class="rateit svg rateit-large" data-rateit-min="0" data-rateit-max="10" data-rateit-step="0.1" data-rateit-resetable="true" data-rateit-readonly="false" data-rateit-backingfld="#edit-rating-advanced" data-rateit-value="" data-rateit-starheight="32" data-rateit-starwidth="32">');
+    $this->assertRaw('<div class="rateit svg rateit-large" data-rateit-min="0" data-rateit-max="10" data-rateit-step="0.1" data-rateit-resetable="true" data-rateit-readonly="false" data-rateit-backingfld="[data-drupal-selector=&quot;edit-rating-advanced&quot;]" data-rateit-value="" data-rateit-starheight="32" data-rateit-starwidth="32">');
+
+    // Check required rating display.
+    $this->assertRaw('<label for="edit-rating-required" class="js-form-required form-required">rating_required</label>');
+    $this->assertRaw('<input data-drupal-selector="edit-rating-required" type="range" id="edit-rating-required" name="rating_required" value="0" step="1" min="0" max="5" class="form-webform-rating required" required="required" aria-required="true" />');
+    $this->assertRaw('<div class="rateit svg rateit-medium" data-rateit-min="0" data-rateit-max="5" data-rateit-step="1" data-rateit-resetable="false" data-rateit-readonly="false" data-rateit-backingfld="[data-drupal-selector=&quot;edit-rating-required&quot;]" data-rateit-value="" data-rateit-starheight="24" data-rateit-starwidth="24"></div>');
 
     // Check processing.
     $edit = [
-      'rating_basic' => '4',
+      'rating_basic' => '1',
+      'rating_advanced' => '2',
+      'rating_required' => '3',
     ];
     $this->drupalPostForm('webform/test_element_rating', $edit, t('Submit'));
-    $this->assertRaw("rating_basic: '4'");
+    $this->assertRaw("rating_basic: '1'
+rating_advanced: '2'
+rating_required: '3'");
+
+    // Check required validation.
+    $this->drupalPostForm('webform/test_element_rating', [], t('Submit'));
+    $this->assertRaw('rating_required field is required.');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementReadonlyTest.php b/web/modules/webform/src/Tests/Element/WebformElementReadonlyTest.php
index debd284e4cc2623d49c6654112dbc4cfa128f937..c42b321ac11d5976e4aa01eaa3d7ab5af9e772a1 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementReadonlyTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementReadonlyTest.php
@@ -20,7 +20,7 @@ class WebformElementReadonlyTest extends WebformElementTestBase {
    * Tests element readonly.
    */
   public function testReadonly() {
-    $this->drupalGet('webform/test_element_readonly');
+    $this->drupalGet('/webform/test_element_readonly');
 
     $this->assertRaw('<div class="webform-readonly js-form-item form-item js-form-type-textfield form-type-textfield js-form-item-textfield form-item-textfield">');
     $this->assertRaw('<input readonly="readonly" data-drupal-selector="edit-textfield" type="text" id="edit-textfield" name="textfield" value="" size="60" maxlength="255" class="form-text" />');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementSectionTest.php b/web/modules/webform/src/Tests/Element/WebformElementSectionTest.php
index 332c776acd70c596767c1e1f4f70ca1a0fad0790..9b60c96450da8ca78774f6161d8446707093ca89 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementSectionTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementSectionTest.php
@@ -20,33 +20,29 @@ class WebformElementSectionTest extends WebformElementTestBase {
    * Test element section.
    */
   public function testSection() {
-    $this->drupalGet('webform/test_element_section');
+    $this->drupalGet('/webform/test_element_section');
 
     // Check section element.
-    $this->assertRaw('<section data-drupal-selector="edit-webform-section" aria-describedby="edit-webform-section--description" id="edit-webform-section" class="js-form-item form-item js-form-wrapper form-wrapper webform-section">');
-
-    // Check section help.
-    $this->assertRaw('<h2 class="webform-section-title">webform_section<a href="#help" title="{This is help text}" data-webform-help="{This is help text}" class="webform-element-help">?</a>');
-
-    // Check section description.
-    $this->assertRaw('<div id="edit-webform-section--description" class="description">{This is a description}</div>');
-
-    // Check section required.
-    $this->assertRaw('<section data-drupal-selector="edit-webform-section-required" id="edit-webform-section-required" class="required js-form-item form-item js-form-wrapper form-wrapper webform-section" required="required" aria-required="true">');
-    $this->assertRaw('<h2 class="webform-section-title js-form-required form-required">webform_section_required</h2>');
+    $this->assertRaw('<section data-drupal-selector="edit-webform-section" aria-describedby="edit-webform-section--description" id="edit-webform-section" class="required js-form-item form-item js-form-wrapper form-wrapper webform-section" required="required" aria-required="true">');
+    $this->assertRaw('<h2 class="webform-section-title js-form-required form-required">webform_section<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;webform_section&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;This is help text.&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
+    $this->assertRaw('<div class="description"><div id="edit-webform-section--description" class="webform-element-description">This is a description.</div>');
+    $this->assertRaw('<div id="edit-webform-section--more" class="js-webform-element-more webform-element-more">');
 
     // Check custom h5 title tag.
     $this->assertRaw('<section data-drupal-selector="edit-webform-section-title-custom" id="edit-webform-section-title-custom" class="js-form-item form-item js-form-wrapper form-wrapper webform-section">');
     $this->assertRaw('<h5 style="color: red" class="webform-section-title">webform_section_title_custom</h5>');
 
+    // Check section title_display: invisible.
+    $this->assertRaw('<h2 class="visually-hidden webform-section-title">webform_section_title_invisible</h2>');
+
     // Check change default title tag.
     \Drupal::configFactory()->getEditable('webform.settings')
       ->set('element.default_section_title_tag', 'address')
       ->save();
 
-    $this->drupalGet('webform/test_element_section');
-    $this->assertNoRaw('<h2 class="webform-section-title">');
-    $this->assertRaw('<address class="webform-section-title">');
+    $this->drupalGet('/webform/test_element_section');
+    $this->assertNoRaw('<h2 class="webform-section-title js-form-required form-required">');
+    $this->assertRaw('<address class="webform-section-title js-form-required form-required">');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementSelectTest.php b/web/modules/webform/src/Tests/Element/WebformElementSelectTest.php
index ac591492adf6706ff4ca36180a8aba61d673b212..e67083ac7bdd7b610784e8798213d44d2b510081 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementSelectTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementSelectTest.php
@@ -21,7 +21,7 @@ class WebformElementSelectTest extends WebformElementTestBase {
    */
   public function testSelectElement() {
     // Check default empty option always included.
-    $this->drupalGet('webform/test_element_select');
+    $this->drupalGet('/webform/test_element_select');
     $this->assertRaw('<select data-drupal-selector="edit-select-empty-option-optional" id="edit-select-empty-option-optional" name="select_empty_option_optional" class="form-select"><option value="" selected="selected">- None -</option>');
     $this->assertRaw('<select data-drupal-selector="edit-select-empty-option-optional-default-value" id="edit-select-empty-option-optional-default-value" name="select_empty_option_optional_default_value" class="form-select"><option value="">- None -</option>');
     $this->assertRaw('<select data-drupal-selector="edit-select-empty-option-required" id="edit-select-empty-option-required" name="select_empty_option_required" class="form-select required" required="required" aria-required="true"><option value="" selected="selected">- Select -</option>');
@@ -32,7 +32,7 @@ public function testSelectElement() {
       ->save();
 
     // Check default empty option is not always included.
-    $this->drupalGet('webform/test_element_select');
+    $this->drupalGet('/webform/test_element_select');
     $this->assertNoRaw('<select data-drupal-selector="edit-select-empty-option-optional" id="edit-select-empty-option-optional" name="select_empty_option_optional" class="form-select"><option value="" selected="selected">- None -</option>');
     $this->assertNoRaw('<select data-drupal-selector="edit-select-empty-option-optional-default-value" id="edit-select-empty-option-optional-default-value" name="select_empty_option_optional_default_value" class="form-select"><option value="">- None -</option>');
     $this->assertRaw('<select data-drupal-selector="edit-select-empty-option-required" id="edit-select-empty-option-required" name="select_empty_option_required" class="form-select required" required="required" aria-required="true"><option value="" selected="selected">- Select -</option>');
@@ -45,7 +45,7 @@ public function testSelectElement() {
       ->save();
 
     // Check customize empty option displayed.
-    $this->drupalGet('webform/test_element_select');
+    $this->drupalGet('/webform/test_element_select');
     $this->assertRaw('<select data-drupal-selector="edit-select-empty-option-optional" id="edit-select-empty-option-optional" name="select_empty_option_optional" class="form-select"><option value="" selected="selected">{optional}</option>');
     $this->assertRaw('<select data-drupal-selector="edit-select-empty-option-optional-default-value" id="edit-select-empty-option-optional-default-value" name="select_empty_option_optional_default_value" class="form-select"><option value="">{optional}</option>');
     $this->assertRaw('<select data-drupal-selector="edit-select-empty-option-required" id="edit-select-empty-option-required" name="select_empty_option_required" class="form-select required" required="required" aria-required="true"><option value="" selected="selected">{required}</option>');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementSignatureTest.php b/web/modules/webform/src/Tests/Element/WebformElementSignatureTest.php
index 44228be7c1c62c93c818f250f89ba7ce95004b1c..470b95b504a4f31fc150eb8d7f24f329d2fce4e3 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementSignatureTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementSignatureTest.php
@@ -31,13 +31,12 @@ public function testSignature() {
     $signature_directory = 'public://webform/test_element_signature/signature';
 
     // Check signature display.
-    $this->drupalGet('webform/test_element_signature');
+    $this->drupalGet('/webform/test_element_signature');
     $this->assertRaw('<input data-drupal-selector="edit-signature" aria-describedby="edit-signature--description" type="hidden" name="signature" value="" class="js-webform-signature form-webform-signature" />');
     $this->assertRaw('<input type="submit" name="op" value="Reset" class="button js-form-submit form-submit" />');
     $this->assertRaw('<canvas></canvas>');
     $this->assertRaw('</div>');
-    $this->assertRaw('<div id="edit-signature--description" class="description">');
-    $this->assertRaw('Sign above');
+    $this->assertRaw('<div id="edit-signature--description" class="webform-element-description">Sign above</div>');
 
     // Check signature preview image.
     $this->postSubmissionTest($webform, [], t('Preview'));
diff --git a/web/modules/webform/src/Tests/Element/WebformElementStatesSelectorsTest.php b/web/modules/webform/src/Tests/Element/WebformElementStatesSelectorsTest.php
index 3c6870e6c7592b16c34a9e627860c1a7294c575d..1f9e16bc4afc44dbda19251d90cf4d25cf475951 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementStatesSelectorsTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementStatesSelectorsTest.php
@@ -36,6 +36,10 @@ public function setUp() {
     // Create 'tags' vocabulary.
     $this->createTags();
 
+    \Drupal::configFactory()->getEditable('webform.settings')
+      ->set('libraries.excluded_libraries', [])
+      ->save();
+
     // Enable all elements, including password and password_confirm.
     \Drupal::configFactory()->getEditable('webform.settings')
       ->set('element.excluded_elements', [])
@@ -51,7 +55,7 @@ public function testSelectors() {
       $webform = Webform::load($webform_id);
       $webform->setStatus(WebformInterface::STATUS_OPEN)->save();
 
-      $this->drupalGet('webform/' . $webform_id);
+      $this->drupalGet('/webform/' . $webform_id);
 
       $selectors = OptGroup::flattenOptions($webform->getElementsSelectorOptions());
       // Ignore text format and captcha selectors which are not available during
diff --git a/web/modules/webform/src/Tests/Element/WebformElementStatesTest.php b/web/modules/webform/src/Tests/Element/WebformElementStatesTest.php
index 52c85e80c3e78aac270ae90d50d7a432be513d43..7b4f07221f68684242d573f5f14acad8519318cb 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementStatesTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementStatesTest.php
@@ -87,7 +87,7 @@ public function testElement() {
     // Rendering.
     /**************************************************************************/
 
-    $this->drupalGet('webform/test_element_states');
+    $this->drupalGet('/webform/test_element_states');
 
     // Check 'States custom selector'.
     $this->assertRaw('<option value="custom_selector" selected="selected">custom_selector</option>');
@@ -101,8 +101,22 @@ public function testElement() {
     $this->assertRaw('<textarea data-drupal-selector="edit-states-unsupported-nesting-states" aria-describedby="edit-states-unsupported-nesting-states--description" class="js-webform-codemirror webform-codemirror yaml form-textarea resize-vertical" data-webform-codemirror-mode="text/x-yaml" id="edit-states-unsupported-nesting-states" name="states_unsupported_nesting[states]" rows="5" cols="60">');
 
     // Check 'States single' (#multiple: FALSE)
-    $this->assertFieldById('edit-states-empty-add');
-    $this->assertNoFieldById('edit-states-single-add');
+    $this->assertFieldById('edit-states-empty-actions-add');
+    $this->assertNoFieldById('edit-states-single-actions-add');
+
+    /**************************************************************************/
+    // Validation.
+    /**************************************************************************/
+
+    // Check duplicate states validation.
+    $edit = ['states_basic[states][0][state]' => 'required'];
+    $this->drupalPostForm('webform/test_element_states', $edit, t('Submit'));
+    $this->assertRaw('The <em class="placeholder">Required</em> state is declared more than once. There can only be one declaration per state.');
+
+    // Check duplicate selectors validation.
+    $edit = ['states_basic[states][3][selector]' => 'selector_02'];
+    $this->drupalPostForm('webform/test_element_states', $edit, t('Submit'));
+    $this->assertRaw('The <em class="placeholder">Selector 02 (selector_02)</em> element is used more than once within the <em class="placeholder">Required</em> state. To use multiple values within a trigger try using the pattern trigger.');
 
     /**************************************************************************/
     // Processing.
@@ -156,6 +170,25 @@ public function testElement() {
     $this->assertNoFieldByName('states_empty[states][2][selector]', 'selector_02');
     $this->assertNoFieldByName('states_empty[states][2][trigger]', 'value');
     $this->assertNoFieldByName('states_empty[states][2][value]', '{value_02}');
+
+    /**************************************************************************/
+    // Edit source.
+    /**************************************************************************/
+
+    // Check that  'Edit source' button is not available.
+    $this->drupalGet('/webform/test_element_states');
+    $this->assertNoRaw('<input class="button button--danger js-form-submit form-submit" data-drupal-selector="edit-states-basic-actions-source" formnovalidate="formnovalidate" type="submit" id="edit-states-basic-actions-source" name="states_basic_table_source" value="Edit source" />');
+
+    // Check that  'Edit source' button is available.
+    $this->drupalLogin($this->rootUser);
+    $this->drupalGet('/webform/test_element_states');
+    $this->assertRaw('<input class="button button--danger js-form-submit form-submit" data-drupal-selector="edit-states-basic-actions-source" formnovalidate="formnovalidate" type="submit" id="edit-states-basic-actions-source" name="states_basic_table_source" value="Edit source" />');
+    $this->assertNoFieldByName('states_basic[states]');
+
+    // Check that 'source' is editable.
+    $this->drupalPostAjaxForm(NULL, [], 'states_basic_table_source');
+    $this->assertRaw('Creating custom conditional logic (Form API #states) with nested conditions or custom selectors will disable the conditional logic builder. This will require that Form API #states be manually entered.');
+    $this->assertFieldByName('states_basic[states]');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementSubmissionViewsReplaceTest.php b/web/modules/webform/src/Tests/Element/WebformElementSubmissionViewsReplaceTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a1a18ddae0ba11d991ee286878becf8442b64397
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementSubmissionViewsReplaceTest.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+/**
+ * Tests for webform submission views replace element.
+ *
+ * @group Webform
+ */
+class WebformElementSubmissionViewsReplaceTest extends WebformElementTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['views', 'node', 'webform', 'webform_node'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_submission_views_r'];
+
+  /**
+   * Test webform submission views replace element.
+   */
+  public function testSubmissionViewsReplace() {
+    // Check rendering.
+    $this->drupalGet('/webform/test_element_submission_views_r');
+    $this->assertRaw('<fieldset data-drupal-selector="edit-webform-submission-views-replace-global-global-routes" id="edit-webform-submission-views-replace-global-global-routes--wrapper" class="fieldgroup form-composite js-form-item form-item js-form-wrapper form-wrapper">');
+    $this->assertRaw('<fieldset data-drupal-selector="edit-webform-submission-views-replace-global-webform-routes" id="edit-webform-submission-views-replace-global-webform-routes--wrapper" class="fieldgroup form-composite js-form-item form-item js-form-wrapper form-wrapper">');
+    $this->assertRaw('<fieldset data-drupal-selector="edit-webform-submission-views-replace-global-node-routes" id="edit-webform-submission-views-replace-global-node-routes--wrapper" class="fieldgroup form-composite js-form-item form-item js-form-wrapper form-wrapper">');
+
+    // Check that the webform replace element is hidden.
+    $this->assertNoRaw('<fieldset data-drupal-selector="edit-webform-submission-views-replace-webform-routes" id="edit-webform-submission-views-replace-webform-routes--wrapper" class="fieldgroup form-composite js-form-item form-item js-form-wrapper form-wrapper">');
+    $this->assertNoRaw('<fieldset data-drupal-selector="edit-webform-submission-views-replace-node-routes" id="edit-webform-submission-views-replace-node-routes--wrapper" class="fieldgroup form-composite js-form-item form-item js-form-wrapper form-wrapper">');
+
+    // Check processing clears hidden webform_submission_views_replace.
+    $this->drupalPostForm('webform/test_element_submission_views_r', [], t('Submit'));
+    $this->assertRaw("webform_submission_views_replace_global:
+  global_routes:
+    - entity.webform_submission.collection
+  webform_routes:
+    - entity.webform.results_submissions
+  node_routes:
+    - entity.node.webform.results_submissions
+webform_submission_views_replace:
+  webform_routes: {  }
+  node_routes: {  }");
+
+    // Clear default_submission_views_replace.
+    \Drupal::configFactory()
+      ->getEditable('webform.settings')
+      ->set('settings.default_submission_views_replace', [
+        'global_routes' => [],
+        'webform_routes' => [],
+        'node_routes' => [],
+      ])
+      ->save();
+
+    // Check that the webform replace element is visible.
+    $this->drupalGet('/webform/test_element_submission_views_r');
+    $this->assertRaw('<fieldset data-drupal-selector="edit-webform-submission-views-replace-webform-routes" id="edit-webform-submission-views-replace-webform-routes--wrapper" class="fieldgroup form-composite js-form-item form-item js-form-wrapper form-wrapper">');
+    $this->assertRaw('<fieldset data-drupal-selector="edit-webform-submission-views-replace-node-routes" id="edit-webform-submission-views-replace-node-routes--wrapper" class="fieldgroup form-composite js-form-item form-item js-form-wrapper form-wrapper">');
+
+    // Check processing with webform replace element is visible.
+    $this->drupalPostForm('webform/test_element_submission_views_r', [], t('Submit'));
+    $this->assertRaw("webform_submission_views_replace_global:
+  global_routes:
+    - entity.webform_submission.collection
+  webform_routes:
+    - entity.webform.results_submissions
+  node_routes:
+    - entity.node.webform.results_submissions
+webform_submission_views_replace:
+  webform_routes:
+    - entity.webform.results_submissions
+  node_routes:
+    - entity.node.webform.results_submissions");
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementSubmissionViewsTest.php b/web/modules/webform/src/Tests/Element/WebformElementSubmissionViewsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..725abc823cf74a2083f9ae18d7f9910af1da7989
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementSubmissionViewsTest.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+/**
+ * Tests for webform submission views element.
+ *
+ * @group Webform
+ */
+class WebformElementSubmissionViewsTest extends WebformElementTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['views', 'node', 'webform', 'webform_node'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_submission_views'];
+
+  /**
+   * Test webform submission views element.
+   */
+  public function testSubmissionViews() {
+    // Check global and webform rendering.
+    $this->drupalGet('/webform/test_element_submission_views');
+    $this->assertRaw('<th class="webform_submission_views_global-table--name_title_view webform-multiple-table--name_title_view">');
+    $this->assertRaw('<th class="webform_submission_views_global-table--global_routes webform-multiple-table--global_routes">');
+    $this->assertRaw('<th class="webform_submission_views_global-table--webform_routes webform-multiple-table--webform_routes">');
+    $this->assertRaw('<th class="webform_submission_views_global-table--node_routes webform-multiple-table--node_routes">');
+    $this->assertRaw('<th class="webform_submission_views-table--name_title_view webform-multiple-table--name_title_view">');
+    $this->assertNoRaw('<th class="webform_submission_views-table--global_routes webform-multiple-table--global_routes">');
+    $this->assertRaw('<th class="webform_submission_views-table--webform_routes webform-multiple-table--webform_routes">');
+    $this->assertRaw('<th class="webform_submission_views-table--node_routes webform-multiple-table--node_routes">');
+
+    // Check name validation.
+    $edit = ['webform_submission_views_global[items][0][name]' => ''];
+    $this->drupalPostForm('webform/test_element_submission_views', $edit, t('Submit'));
+    $this->assertRaw('Name is required');
+
+    // Check view validation.
+    $edit = ['webform_submission_views_global[items][0][view]' => ''];
+    $this->drupalPostForm('webform/test_element_submission_views', $edit, t('Submit'));
+    $this->assertRaw('View name/display id is required.');
+
+    // Check title validation.
+    $edit = ['webform_submission_views_global[items][0][title]' => ''];
+    $this->drupalPostForm('webform/test_element_submission_views', $edit, t('Submit'));
+    $this->assertRaw('Title is required.');
+
+    // Check processing.
+    $this->drupalPostForm('webform/test_element_submission_views', [], t('Submit'));
+    $this->assertRaw("webform_submission_views_global:
+  admin:
+    view: 'webform_submissions:embed_administer'
+    title: Admin
+    global_routes:
+      - entity.webform_submission.collection
+    webform_routes:
+      - entity.webform.results_submissions
+    node_routes:
+      - entity.node.webform.results_submissions
+webform_submission_views:
+  admin:
+    view: 'webform_submissions:embed_administer'
+    title: Admin
+    webform_routes:
+      - entity.webform.results_submissions
+    node_routes:
+      - entity.node.webform.results_submissions");
+
+    // Check processing empty record.
+    $edit = [
+      'webform_submission_views_global[items][0][name]' => '',
+      'webform_submission_views_global[items][0][view]' => '',
+      'webform_submission_views_global[items][0][title]' => '',
+      'webform_submission_views_global[items][0][global_routes][entity.webform_submission.collection]' => FALSE,
+      'webform_submission_views_global[items][0][webform_routes][entity.webform.results_submissions]' => FALSE,
+      'webform_submission_views_global[items][0][node_routes][entity.node.webform.results_submissions]' => FALSE,
+    ];
+    $this->drupalPostForm('webform/test_element_submission_views', $edit, t('Submit'));
+    $this->assertNoRaw('Name is required');
+    $this->assertNoRaw('View name/display id is required.');
+    $this->assertNoRaw('Title is required.');
+    $this->assertRaw("webform_submission_views_global: {  }
+webform_submission_views:
+  admin:
+    view: 'webform_submissions:embed_administer'
+    title: Admin
+    webform_routes:
+      - entity.webform.results_submissions
+    node_routes:
+      - entity.node.webform.results_submissions");
+
+    // Uninstall the webform node module.
+    $this->container->get('module_installer')->uninstall(['webform_node']);
+
+    // Check global and webform rendering without node settings.
+    $this->drupalGet('/webform/test_element_submission_views');
+    $this->assertNoRaw('<th class="webform_submission_views_global-table--node_routes webform-multiple-table--node_routes">');
+    $this->assertNoRaw('<th class="webform_submission_views-table--node_routes webform-multiple-table--node_routes">');
+
+    // Check processing removes node settings.
+    $this->drupalPostForm('webform/test_element_submission_views', [], t('Submit'));
+    $this->assertRaw("webform_submission_views_global:
+  admin:
+    view: 'webform_submissions:embed_administer'
+    title: Admin
+    global_routes:
+      - entity.webform_submission.collection
+    webform_routes:
+      - entity.webform.results_submissions
+webform_submission_views:
+  admin:
+    view: 'webform_submissions:embed_administer'
+    title: Admin
+    webform_routes:
+      - entity.webform.results_submissions");
+
+    // Uninstall the views module.
+    $this->container->get('module_installer')->uninstall(['views']);
+
+    // Check that element is completely hidden.
+    $this->drupalGet('/webform/test_element_submission_views');
+    $this->assertNoRaw('<th class="webform_submission_views_global-table--name_title_view webform-multiple-table--name_title_view">');
+    $this->assertNoRaw('<th class="webform_submission_views-table--name_title_view webform-multiple-table--name_title_view">');
+
+    // Check that value is preserved.
+    $this->drupalPostForm('webform/test_element_submission_views', [], t('Submit'));
+    $this->assertRaw("webform_submission_views_global: {  }
+webform_submission_views: {  }");
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementTableTest.php b/web/modules/webform/src/Tests/Element/WebformElementTableTest.php
index ab8c11c9152e3ff9983e0bd0bf62ef724895fccb..40c6bd7d0b8dddf0902d75cca0799ca36a26177a 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementTableTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementTableTest.php
@@ -30,7 +30,7 @@ public function testTable() {
     /**************************************************************************/
 
     // Check display elements within a table.
-    $this->drupalGet('webform/test_element_table');
+    $this->drupalGet('/webform/test_element_table');
     $this->assertRaw('<table class="js-form-wrapper responsive-enabled" data-drupal-selector="edit-table" id="edit-table" data-striping="1">');
     $this->assertRaw('<th>First Name</th>');
     $this->assertRaw('<th>Last Name</th>');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementTelephoneTest.php b/web/modules/webform/src/Tests/Element/WebformElementTelephoneTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..78fd05c20ea53fb49203079498e24de5cbb66923
--- /dev/null
+++ b/web/modules/webform/src/Tests/Element/WebformElementTelephoneTest.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\webform\Tests\Element;
+
+/**
+ * Tests for telephone element.
+ *
+ * @group Webform
+ */
+class WebformElementTelephoneTest extends WebformElementTestBase {
+
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'telephone_validation'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_element_telephone'];
+
+  /**
+   * Test telephone element.
+   */
+  public function testRating() {
+    $this->drupalGet('/webform/test_element_telephone');
+
+    // Check basic tel.
+    $this->assertRaw('<input data-drupal-selector="edit-tel-default" type="tel" id="edit-tel-default" name="tel_default" value="" size="30" maxlength="128" class="form-tel" />');
+
+    // Check international tel.
+    $this->assertRaw('<input class="js-webform-telephone-international webform-webform-telephone-international form-tel" data-drupal-selector="edit-tel-international" type="tel" id="edit-tel-international" name="tel_international" value="" size="30" maxlength="128" />');
+
+    // Check international telephone valddation.
+    $this->assertRaw('<input data-drupal-selector="edit-tel-validation-e164" type="tel" id="edit-tel-validation-e164" name="tel_validation_e164" value="" size="30" maxlength="128" class="form-tel" />');
+
+    // Check USE telephone validation.
+    $this->assertRaw('<input data-drupal-selector="edit-tel-validation-national" aria-describedby="edit-tel-validation-national--description" type="tel" id="edit-tel-validation-national" name="tel_validation_national" value="" size="30" maxlength="128" class="form-tel" />');
+
+    // Check telephone validation missing plus sign.
+    $edit = [
+      'tel_validation_e164' => '12024561111',
+      'tel_validation_national' => '12024561111',
+    ];
+    $this->drupalPostForm('webform/test_element_telephone', $edit, t('Submit'));
+    $this->assertRaw('The phone number <em class="placeholder">12024561111</em> is not valid.');
+
+    // Check telephone validation with plus sign.
+    $edit = [
+      'tel_validation_e164' => '+12024561111',
+      'tel_validation_national' => '+12024561111',
+    ];
+    $this->drupalPostForm('webform/test_element_telephone', $edit, t('Submit'));
+    $this->assertNoRaw('The phone number <em class="placeholder">12024561111</em> is not valid.');
+
+    // Check telephone validation with non US number.
+    $edit = [
+      'tel_validation_national' => '+74956970349',
+    ];
+    $this->drupalPostForm('webform/test_element_telephone', $edit, t('Submit'));
+    $this->assertNoRaw('The phone number <em class="placeholder">+74956970349</em> is not valid.');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementTermReferenceTest.php b/web/modules/webform/src/Tests/Element/WebformElementTermReferenceTest.php
index 5e666d2aa27608ec36ed188317eb1785b62f8459..73f9db4261b9e05ad661377dc7680c14bcc9f57b 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementTermReferenceTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementTermReferenceTest.php
@@ -45,16 +45,16 @@ public function testTermReference() {
     // Term checkboxes
     /**************************************************************************/
 
-    $this->drupalGet('webform/test_element_term_reference');
+    $this->drupalGet('/webform/test_element_term_reference');
 
     // Check term checkboxes tree default.
-    $this->assertRaw('<fieldset data-drupal-selector="edit-webform-term-checkboxes-tree-default" class="js-webform-term-checkboxes webform-term-checkboxes webform-term-checkboxes-scroll fieldgroup form-composite js-form-item form-item js-form-wrapper form-wrapper" id="edit-webform-term-checkboxes-tree-default--wrapper">');
+    $this->assertRaw('<fieldset data-drupal-selector="edit-webform-term-checkboxes-tree-default" class="js-webform-term-checkboxes webform-term-checkboxes webform-term-checkboxes-scroll webform-term-checkboxes--wrapper fieldgroup form-composite webform-composite-visible-title js-webform-type-webform-term-checkboxes webform-type-webform-term-checkboxes js-form-item form-item js-form-wrapper form-wrapper" id="edit-webform-term-checkboxes-tree-default--wrapper">');
     $this->assertRaw('<span class="field-prefix">&nbsp;&nbsp;&nbsp;</span>');
     $this->assertRaw('<input data-drupal-selector="edit-webform-term-checkboxes-tree-default-2" type="checkbox" id="edit-webform-term-checkboxes-tree-default-2" name="webform_term_checkboxes_tree_default[2]" value="2" class="form-checkbox" />');
     $this->assertRaw('<label for="edit-webform-term-checkboxes-tree-default-2" class="option">Parent 1: Child 1</label>');
 
     // Check term checkboxes tree advanced.
-    $this->assertRaw('<fieldset data-drupal-selector="edit-webform-term-checkboxes-tree-advanced" class="js-webform-term-checkboxes webform-term-checkboxes fieldgroup form-composite js-form-item form-item js-form-wrapper form-wrapper" id="edit-webform-term-checkboxes-tree-advanced--wrapper">');
+    $this->assertRaw('<fieldset data-drupal-selector="edit-webform-term-checkboxes-tree-advanced" class="js-webform-term-checkboxes webform-term-checkboxes webform-term-checkboxes--wrapper fieldgroup form-composite webform-composite-visible-title js-webform-type-webform-term-checkboxes webform-type-webform-term-checkboxes js-form-item form-item js-form-wrapper form-wrapper" id="edit-webform-term-checkboxes-tree-advanced--wrapper">');
     $this->assertRaw('<span class="field-prefix">..</span>');
     $this->assertRaw('<input data-drupal-selector="edit-webform-term-checkboxes-tree-advanced-2" type="checkbox" id="edit-webform-term-checkboxes-tree-advanced-2" name="webform_term_checkboxes_tree_advanced[2]" value="2" class="form-checkbox" />');
     $this->assertRaw('<label for="edit-webform-term-checkboxes-tree-advanced-2" class="option">Parent 1: Child 1</label>');
@@ -75,7 +75,7 @@ public function testTermReference() {
     // Term select.
     /**************************************************************************/
 
-    $this->drupalGet('webform/test_element_term_reference');
+    $this->drupalGet('/webform/test_element_term_reference');
 
     // Check term select tree default.
     $this->assertRaw('<option value="1">Parent 1</option>');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementTermsOfServiceTest.php b/web/modules/webform/src/Tests/Element/WebformElementTermsOfServiceTest.php
index 9e3c6c0cdd60ab5b56eac3f222975648f9da7fb3..a1f8d3d7594a20ed7336579b160bafc47ab15ebc 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementTermsOfServiceTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementTermsOfServiceTest.php
@@ -28,12 +28,19 @@ class WebformElementTermsOfServiceTest extends WebformElementTestBase {
    */
   public function testTermsOfService() {
     // Check rendering.
-    $this->drupalGet('webform/test_element_terms_of_service');
+    $this->drupalGet('/webform/test_element_terms_of_service');
+
+    // Check modal.
     $this->assertRaw('<div data-webform-terms-of-service-type="modal" class="form-type-webform-terms-of-service js-form-type-webform-terms-of-service js-form-item form-item js-form-type-checkbox form-type-checkbox js-form-item-terms-of-service-default form-item-terms-of-service-default">');
     $this->assertRaw('<input data-drupal-selector="edit-terms-of-service-default" type="checkbox" id="edit-terms-of-service-default" name="terms_of_service_default" value class="form-checkbox required" required="required" aria-required="true" />');
-    $this->assertRaw('<label for="edit-terms-of-service-default" class="option js-form-required form-required">I agree to the <a>terms of service</a>. (default)</label>');
-    $this->assertRaw('<div id="edit-terms-of-service-default--description" class="description">');
-    $this->assertRaw('<div class="webform-terms-of-service-details js-hide"><div class="webform-terms-of-service-details--title">terms_of_service_default</div><div class="webform-terms-of-service-details--content">These are the terms of service.</div></div>');
+    $this->assertRaw('<label for="edit-terms-of-service-default" class="option js-form-required form-required">I agree to the <a role="button" href="#terms">terms of service</a>. (default)</label>');
+    $this->assertRaw('<div id="edit-terms-of-service-default--description" class="webform-element-description">');
+    $this->assertRaw('<div id="webform-terms-of-service-terms_of_service_default--description" class="webform-terms-of-service-details js-hide">');
+    $this->assertRaw('<div class="webform-terms-of-service-details--title">terms_of_service_default</div>');
+    $this->assertRaw('<div class="webform-terms-of-service-details--content">These are the terms of service.</div>');
+
+    // Check slideout.
+    $this->assertRaw('<label for="edit-terms-of-service-slideout" class="option">I agree to the <a role="button" href="#terms">terms of service</a>. (slideout)</label>');
 
     // Check validation.
     $this->drupalPostForm('webform/test_element_terms_of_service', [], t('Preview'));
diff --git a/web/modules/webform/src/Tests/Element/WebformElementTextFormatTest.php b/web/modules/webform/src/Tests/Element/WebformElementTextFormatTest.php
index b404936bbfb389075900b331e9d9d56236ec0c43..382817fc218ac0d2fc1a0605f662e677351b1fae 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementTextFormatTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementTextFormatTest.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\webform\Tests\Element;
 
+use Drupal\file\Entity\File;
 use Drupal\webform\Entity\Webform;
+use Drupal\webform\Entity\WebformSubmission;
 
 /**
  * Tests for text format element.
@@ -16,7 +18,7 @@ class WebformElementTextFormatTest extends WebformElementTestBase {
    *
    * @var array
    */
-  public static $modules = ['filter', 'webform'];
+  public static $modules = ['filter', 'file', 'webform'];
 
   /**
    * Webforms to load.
@@ -25,14 +27,35 @@ class WebformElementTextFormatTest extends WebformElementTestBase {
    */
   protected static $testWebforms = ['test_element_text_format'];
 
+  /**
+   * File usage manager.
+   *
+   * @var \Drupal\file\FileUsage\FileUsageInterface
+   */
+  protected $fileUsage;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->fileUsage = $this->container->get('file.usage');
+  }
+
   /**
    * Test text format element.
    */
   public function testTextFormat() {
-    $webform_text_format = Webform::load('test_element_text_format');
+    $webform = Webform::load('test_element_text_format');
+
+    // Check that formats and tips are removed and/or hidden.
+    $this->drupalGet('/webform/test_element_text_format');
+    $this->assertRaw('<div class="filter-wrapper js-form-wrapper form-wrapper" data-drupal-selector="edit-text-format-format" style="display: none" id="edit-text-format-format">');
+    $this->assertRaw('<div class="filter-help js-form-wrapper form-wrapper" data-drupal-selector="edit-text-format-format-help" style="display: none" id="edit-text-format-format-help">');
 
     // Check 'text_format' values.
-    $this->drupalGet('webform/test_element_text_format');
+    $this->drupalGet('/webform/test_element_text_format');
     $this->assertFieldByName('text_format[value]', 'The quick brown fox jumped over the lazy dog.');
     $this->assertRaw('No HTML tags allowed.');
 
@@ -40,9 +63,123 @@ public function testTextFormat() {
       'value' => 'Custom value',
       'format' => 'custom_format',
     ];
-    $form = $webform_text_format->getSubmissionForm(['data' => ['text_format' => $text_format]]);
+    $form = $webform->getSubmissionForm(['data' => ['text_format' => $text_format]]);
     $this->assertEqual($form['elements']['text_format']['#default_value'], $text_format['value']);
     $this->assertEqual($form['elements']['text_format']['#format'], $text_format['format']);
   }
 
+  /**
+   * Tests webform text format element files.
+   */
+  public function testTextFormatFiles() {
+    $this->createFilters();
+
+    $webform = Webform::load('test_element_text_format');
+
+    $this->drupalLogin($this->rootUser);
+
+    // Create three test images.
+    /** @var \Drupal\file\FileInterface[] $images */
+    $images = $this->drupalGetTestFiles('image');
+    $images = array_slice($images, 0, 5);
+    foreach ($images as $index => $image_file) {
+      $images[$index] = File::create((array) $image_file);
+      $images[$index]->save();
+    }
+    // Check that all images are temporary.
+    $this->assertTrue($images[0]->isTemporary());
+    $this->assertTrue($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+
+    // Upload the first image.
+    $edit = [
+      'text_format[value]' => '<img data-entity-type="file" data-entity-uuid="' . $images[0]->uuid() . '"/>',
+      'text_format[format]' => 'full_html',
+    ];
+    $sid = $this->postSubmission($webform, $edit);
+    $this->reloadImages($images);
+
+    // Check that first image is not temporary.
+    $this->assertFalse($images[0]->isTemporary());
+    $this->assertTrue($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+
+    // Check create first image file usage.
+    $this->assertIdentical(['editor' => ['webform_submission' => [$sid => '1']]], $this->fileUsage->listUsage($images[0]), 'The file has 1 usage.');
+
+    // Upload the second image.
+    $edit = [
+      'text_format[value]' => '<img data-entity-type="file" data-entity-uuid="' . $images[0]->uuid() . '"/><img data-entity-type="file" data-entity-uuid="' . $images[1]->uuid() . '"/>',
+      'text_format[format]' => 'full_html',
+    ];
+    $this->drupalPostForm("/admin/structure/webform/manage/test_element_text_format/submission/$sid/edit", $edit, t('Save'));
+    $this->reloadImages($images);
+
+    // Check that first and second image are not temporary.
+    $this->assertFalse($images[0]->isTemporary());
+    $this->assertFalse($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+
+    // Check first and second image file usage.
+    $this->assertIdentical(['editor' => ['webform_submission' => [$sid => '1']]], $this->fileUsage->listUsage($images[0]), 'The file has 1 usage.');
+    $this->assertIdentical(['editor' => ['webform_submission' => [$sid => '1']]], $this->fileUsage->listUsage($images[1]), 'The file has 1 usage.');
+
+    // Remove the first image.
+    $edit = [
+      'text_format[value]' => '<img data-entity-type="file" data-entity-uuid="' . $images[1]->uuid() . '"/>',
+      'text_format[format]' => 'full_html',
+    ];
+    $this->drupalPostForm("/admin/structure/webform/manage/test_element_text_format/submission/$sid/edit", $edit, t('Save'));
+    $this->reloadImages($images);
+
+    // Check that first is temporary and second image is not temporary.
+    $this->assertTrue($images[0]->isTemporary());
+    $this->assertFalse($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+
+    // Check first and second image file usage.
+    $this->assertIdentical([], $this->fileUsage->listUsage($images[0]), 'The file has 0 usage.');
+    $this->assertIdentical(['editor' => ['webform_submission' => [$sid => '1']]], $this->fileUsage->listUsage($images[1]), 'The file has 1 usage.');
+
+    // Duplicate submission.
+    $webform_submission = WebformSubmission::load($sid);
+    $webform_submission_duplicate = $webform_submission->createDuplicate();
+    $webform_submission_duplicate->save();
+
+    // Check second image file usage.
+    $this->assertIdentical(['editor' => ['webform_submission' => [$webform_submission->id() => '1', $webform_submission_duplicate->id() => '1']]], $this->fileUsage->listUsage($images[1]), 'The file has 2 usages.');
+
+    // Delete the duplicate webform submission.
+    $webform_submission_duplicate->delete();
+
+    // Check second image file usage.
+    $this->assertIdentical(['editor' => ['webform_submission' => [$sid => '1']]], $this->fileUsage->listUsage($images[1]), 'The file has 1 usage.');
+
+    // Delete the webform submission.
+    $this->drupalPostForm("/admin/structure/webform/manage/test_element_text_format/submission/$sid/delete", [], t('Delete'));
+    $this->reloadImages($images);
+
+    // Check that first and second image are temporary.
+    $this->assertTrue($images[0]->isTemporary());
+    $this->assertTrue($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+  }
+
+  /****************************************************************************/
+  // Helper functions.
+  /****************************************************************************/
+
+  /**
+   * Reload images.
+   *
+   * @param array $images
+   *   An array of image files.
+   */
+  protected function reloadImages(array &$images) {
+    \Drupal::entityTypeManager()->getStorage('file')->resetCache();
+    foreach ($images as $index => $image) {
+      $images[$index] = File::load($image->id());
+    }
+  }
+
 }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementTextTest.php b/web/modules/webform/src/Tests/Element/WebformElementTextTest.php
deleted file mode 100644
index 1eed8c47983b5d2299e40676b619acffde07bfd9..0000000000000000000000000000000000000000
--- a/web/modules/webform/src/Tests/Element/WebformElementTextTest.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-namespace Drupal\webform\Tests\Element;
-
-/**
- * Tests for webform text elements.
- *
- * @group Webform
- */
-class WebformElementTextTest extends WebformElementTestBase {
-
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = ['filter', 'webform'];
-
-  /**
-   * Webforms to load.
-   *
-   * @var array
-   */
-  protected static $testWebforms = ['test_element_text'];
-
-  /**
-   * Tests text elements.
-   */
-  public function testTextElements() {
-
-    /**************************************************************************/
-    // text format
-    /**************************************************************************/
-
-    // Check that formats and tips are removed and/or hidden.
-    $this->drupalGet('webform/test_element_text');
-    $this->assertRaw('<div class="filter-wrapper js-form-wrapper form-wrapper" data-drupal-selector="edit-text-format-format" style="display: none" id="edit-text-format-format">');
-    $this->assertRaw('<div class="filter-help js-form-wrapper form-wrapper" data-drupal-selector="edit-text-format-format-help" style="display: none" id="edit-text-format-format-help">');
-
-    /**************************************************************************/
-    // counter
-    /**************************************************************************/
-
-    // Check counters.
-    $this->drupalGet('webform/test_element_text');
-    $this->assertRaw('<input data-counter-type="character" data-counter-limit="10" class="js-webform-counter webform-counter form-text" data-drupal-selector="edit-counter-characters" type="text" id="edit-counter-characters" name="counter_characters" value="" size="60" maxlength="10" />');
-    $this->assertRaw('<textarea data-counter-type="word" data-counter-limit="3" data-counter-message="word(s) left. This is a custom message" class="js-webform-counter webform-counter form-textarea resize-vertical" data-drupal-selector="edit-counter-words" id="edit-counter-words" name="counter_words" rows="5" cols="60"></textarea>');
-
-    // Check counter validation error.
-    $edit = [
-      'counter_characters' => '01234567890',
-      'counter_words' => 'one two three four',
-    ];
-    $this->drupalPostForm('webform/test_element_text', $edit, t('Submit'));
-    $this->assertRaw('Character counter cannot be longer than <em class="placeholder">10</em> characters but is currently <em class="placeholder">11</em> characters long.</li>');
-    $this->assertRaw('Word counter cannot be longer than <em class="placeholder">3</em> words but is currently <em class="placeholder">4</em> words long.');
-
-    // Check counter validation passes.
-    $edit = [
-      'counter_characters' => '0123456789',
-      'counter_words' => 'one two three',
-    ];
-    $this->drupalPostForm('webform/test_element_text', $edit, t('Submit'));
-    $this->assertNoRaw('Character counter cannot be longer than <em class="placeholder">10</em> characters but is currently <em class="placeholder">11</em> characters long.</li>');
-    $this->assertNoRaw('Word counter cannot be longer than <em class="placeholder">3</em> words but is currently <em class="placeholder">4</em> words long.');
-  }
-
-}
diff --git a/web/modules/webform/src/Tests/Element/WebformElementTimeTest.php b/web/modules/webform/src/Tests/Element/WebformElementTimeTest.php
index 4c15d750adb59c2fe335ece08db0b99162651da6..eb151e16c4cc9a5e81e9d8d5514c375dee9bbcb0 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementTimeTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementTimeTest.php
@@ -20,32 +20,56 @@ class WebformElementTimeTest extends WebformElementTestBase {
    * Test time element.
    */
   public function testTime() {
-    $this->drupalGet('webform/test_element_time');
+    $this->drupalGet('/webform/test_element_time');
 
     // Check time element.
     $this->assertRaw('<label for="edit-time-12-hour">time_12_hour</label>');
-    $this->assertRaw('<input data-drupal-selector="edit-time-12-hour" data-webform-time-format="g:i A" type="time" id="edit-time-12-hour" name="time_12_hour" value="14:00" size="10" class="form-time webform-time" />');
+    $this->assertRaw('<input data-drupal-selector="edit-time-12-hour" data-webform-time-format="g:i A" type="time" id="edit-time-12-hour" name="time_12_hour" value="14:00" size="12" maxlength="12" class="form-time webform-time" />');
 
     // Check timepicker elements.
-    $this->assertRaw('<input data-drupal-selector="edit-time-timepicker" data-webform-time-format="g:i A" type="text" id="edit-time-timepicker" name="time_timepicker" value="2:00 PM" size="10" class="form-time webform-time" />');
-    $this->assertRaw('<input data-drupal-selector="edit-time-timepicker-min-max" aria-describedby="edit-time-timepicker-min-max--description" data-webform-time-format="g:i A" type="text" id="edit-time-timepicker-min-max" name="time_timepicker_min_max" value="2:00 PM" size="10" min="14:00" max="18:00" class="form-time webform-time" />');
+    $this->assertRaw('<input data-drupal-selector="edit-time-timepicker" data-webform-time-format="g:i A" type="text" id="edit-time-timepicker" name="time_timepicker" value="2:00 PM" size="12" maxlength="12" class="form-time webform-time" />');
+    $this->assertRaw('<input data-drupal-selector="edit-time-timepicker-min-max" aria-describedby="edit-time-timepicker-min-max--description" data-webform-time-format="g:i A" type="text" id="edit-time-timepicker-min-max" name="time_timepicker_min_max" value="2:00 PM" size="12" maxlength="12" min="14:00" max="18:00" class="form-time webform-time" />');
+
+    // Check time processing.
+    $this->drupalPostForm('webform/test_element_time', [], t('Submit'));
+    $time_12_hour_plus_6_hours = date('H:i:00', strtotime('+6 hours'));
+    $this->assertRaw("time_default: '14:00:00'
+time_24_hour: '14:00:00'
+time_12_hour: '14:00:00'
+time_12_hour_plus_6_hours: '$time_12_hour_plus_6_hours'
+time_steps: '14:00:00'
+time_min_max: '14:00:00'
+time_timepicker: '14:00:00'
+time_timepicker_min_max: '14:00:00'");
 
     // Check time validation.
     $edit = ['time_24_hour' => 'not-valid'];
     $this->drupalPostForm('webform/test_element_time', $edit, t('Submit'));
     $this->assertRaw('<em class="placeholder">time_24_hour</em> must be a valid time.');
 
+    // Check '0' string trigger validation error.
+    $edit = ['time_default' => '0'];
+    $this->drupalPostForm('webform/test_element_time', $edit, t('Submit'));
+    $this->assertRaw('em class="placeholder">time_default</em> must be a valid time.');
+
+    // Check '++' string (faulty relative date) trigger validation error.
+    $edit = ['time_default' => '++'];
+    $this->drupalPostForm('webform/test_element_time', $edit, t('Submit'));
+    $this->assertRaw('em class="placeholder">time_default</em> must be a valid time.');
+
+    // Check empty string trigger does not validation error.
+    $edit = ['time_default' => ''];
+    $this->drupalPostForm('webform/test_element_time', $edit, t('Submit'));
+    $this->assertNoRaw('<em class="placeholder">time_default</em> must be a valid time.');
+    $this->assertRaw("time_default: ''");
+
     // Check time #max validation.
-    $edit = [
-      'time_min_max' => '12:00',
-    ];
+    $edit = ['time_min_max' => '12:00'];
     $this->drupalPostForm('webform/test_element_time', $edit, t('Submit'));
     $this->assertRaw('<em class="placeholder">time_min_max</em> must be on or after <em class="placeholder">14:00</em>.');
 
     // Check time #min validation.
-    $edit = [
-      'time_min_max' => '22:00',
-    ];
+    $edit = ['time_min_max' => '22:00'];
     $this->drupalPostForm('webform/test_element_time', $edit, t('Submit'));
     $this->assertRaw('<em class="placeholder">time_min_max</em> must be on or before <em class="placeholder">18:00</em>.');
   }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementToggleTest.php b/web/modules/webform/src/Tests/Element/WebformElementToggleTest.php
index 0d5d9d63be4f6d5e3f8bead957c54133dbb47738..a0c6be59d841f016bb99ff6a46b25c4208a195d1 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementToggleTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementToggleTest.php
@@ -16,11 +16,21 @@ class WebformElementToggleTest extends WebformElementTestBase {
    */
   protected static $testWebforms = ['test_element_toggle'];
 
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->config('webform.settings')
+      ->set('libraries.excluded_libraries', [])
+      ->save();
+  }
+
   /**
    * Test toggle element.
    */
   public function testToggleElement() {
-    $this->drupalGet('webform/test_element_toggle');
+    $this->drupalGet('/webform/test_element_toggle');
 
     // Check basic toggle.
     $this->assertRaw('<div class="js-form-item form-item js-form-type-webform-toggle form-type-webform-toggle js-form-item-toggle-basic form-item-toggle-basic">');
@@ -35,14 +45,14 @@ public function testToggleElement() {
     $this->assertRaw('<div class="js-webform-toggle webform-toggle toggle toggle-large toggle-iphone" data-toggle-height="36" data-toggle-width="108" data-toggle-text-on="Yes" data-toggle-text-off="No"></div>');
 
     // Check basic toggles.
-    $this->assertRaw('<fieldset data-drupal-selector="edit-toggles-basic" id="edit-toggles-basic--wrapper" class="fieldgroup form-composite js-form-item form-item js-form-wrapper form-wrapper">');
+    $this->assertRaw('<fieldset data-drupal-selector="edit-toggles-basic" id="edit-toggles-basic--wrapper" class="webform-toggles--wrapper fieldgroup form-composite webform-composite-visible-title js-webform-type-webform-toggles webform-type-webform-toggles js-form-item form-item js-form-wrapper form-wrapper">');
     $this->assertRaw('<span class="fieldset-legend">Basic toggles</span>');
     $this->assertRaw('<div id="edit-toggles-basic" class="js-webform-webform-toggles form-checkboxes"><div class="js-form-item form-item js-form-type-webform-toggle form-type-webform-toggle js-form-item-toggles-basic-one form-item-toggles-basic-one">');
     $this->assertRaw('<input data-drupal-selector="edit-toggles-basic-one" type="checkbox" id="edit-toggles-basic-one" name="toggles_basic[one]" value="one" class="form-checkbox" /><div class="js-webform-toggle webform-toggle toggle toggle-medium toggle-light" data-toggle-height="24" data-toggle-width="48" data-toggle-text-on="" data-toggle-text-off=""></div>');
     $this->assertRaw('<label for="edit-toggles-basic-one" class="option">One</label>');
 
     // Check advanced toggles.
-    $this->assertRaw('<fieldset data-drupal-selector="edit-toggles-advanced" id="edit-toggles-advanced--wrapper" class="fieldgroup form-composite js-form-item form-item js-form-wrapper form-wrapper">');
+    $this->assertRaw('<fieldset data-drupal-selector="edit-toggles-advanced" id="edit-toggles-advanced--wrapper" class="webform-toggles--wrapper fieldgroup form-composite webform-composite-visible-title js-webform-type-webform-toggles webform-type-webform-toggles js-form-item form-item js-form-wrapper form-wrapper">');
     $this->assertRaw('<span class="fieldset-legend">Advanced toggles</span>');
     $this->assertRaw('<div id="edit-toggles-advanced" class="js-webform-webform-toggles form-checkboxes"><div class="js-form-item form-item js-form-type-webform-toggle form-type-webform-toggle js-form-item-toggles-advanced-one form-item-toggles-advanced-one">');
     $this->assertRaw('<input data-drupal-selector="edit-toggles-advanced-one" type="checkbox" id="edit-toggles-advanced-one" name="toggles_advanced[one]" value="one" class="form-checkbox" /><div class="js-webform-toggle webform-toggle toggle toggle-large toggle-iphone" data-toggle-height="36" data-toggle-width="108" data-toggle-text-on="Yes" data-toggle-text-off="No"></div>');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementValidateMinlengthTest.php b/web/modules/webform/src/Tests/Element/WebformElementValidateMinlengthTest.php
index c80f0418e17707ec8751424e7280cb8e5a35e1f1..cd1053db95120b2325e13c8be679205fc2143e30 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementValidateMinlengthTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementValidateMinlengthTest.php
@@ -27,6 +27,15 @@ public function testValidateMinlength() {
     // Check minlength validation.
     $this->postSubmission($webform, ['minlength_textfield' => 'X']);
     $this->assertRaw('<em class="placeholder">minlength_textfield</em> cannot be less than <em class="placeholder">5</em> characters but is currently <em class="placeholder">1</em> characters long.');
+
+    // Check minlength not required validation.
+    $this->postSubmission($webform, ['minlength_textfield' => '']);
+    $this->assertNoRaw('<em class="placeholder">minlength_textfield</em> cannot be less than <em class="placeholder">5</em> characters but is currently <em class="placeholder">0</em> characters long.');
+
+    // Check minlength required validation.
+    $this->postSubmission($webform, ['minlength_textfield_required' => '']);
+    $this->assertNoRaw('<em class="placeholder">minlength_textfield_required</em> cannot be less than <em class="placeholder">5</em> characters but is currently <em class="placeholder">0</em> characters long.');
+    $this->assertRaw('minlength_textfield_required field is required.');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Element/WebformElementValidateMultipleTest.php b/web/modules/webform/src/Tests/Element/WebformElementValidateMultipleTest.php
index cfc72a07da3a043722e730399365b5c52c04e289..d755be5ff296392e27775e3e56e27235ffca37c5 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementValidateMultipleTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementValidateMultipleTest.php
@@ -20,10 +20,21 @@ class WebformElementValidateMultipleTest extends WebformElementTestBase {
    * Tests element validate multiple.
    */
   public function testValidateMultiple() {
-    $this->drupalGet('webform/test_element_validate_multiple');
+    $this->drupalGet('/webform/test_element_validate_multiple');
 
     // Check that only three textfields are displayed.
     $this->assertFieldByName('webform_element_multiple_textfield_three[items][0][_item_]');
+    $this->assertNoFieldByName('webform_element_multiple_textfield_three[items][1][_item_]');
+    $this->assertNoFieldByName('webform_element_multiple_textfield_three[items][2][_item_]');
+    $this->assertNoFieldByName('webform_element_multiple_textfield_three[items][3][_item_]');
+    $this->assertNoFieldByName('webform_element_multiple_textfield_three_table_add');
+
+    // Add 2 more items.
+    $edit = [
+      'webform_element_multiple_textfield_three[add][more_items]' => 2,
+    ];
+    $this->drupalPostAjaxForm(NULL, $edit, 'webform_element_multiple_textfield_three_table_add');
+    $this->assertFieldByName('webform_element_multiple_textfield_three[items][0][_item_]');
     $this->assertFieldByName('webform_element_multiple_textfield_three[items][1][_item_]');
     $this->assertFieldByName('webform_element_multiple_textfield_three[items][2][_item_]');
     $this->assertNoFieldByName('webform_element_multiple_textfield_three[items][3][_item_]');
diff --git a/web/modules/webform/src/Tests/Element/WebformElementValidateUniqueTest.php b/web/modules/webform/src/Tests/Element/WebformElementValidateUniqueTest.php
index 32911ec12d7b6671af749ce4c0e8f6f62075a5d5..1b847396a0ea73a0f761ad6023f072a99d31aec0 100644
--- a/web/modules/webform/src/Tests/Element/WebformElementValidateUniqueTest.php
+++ b/web/modules/webform/src/Tests/Element/WebformElementValidateUniqueTest.php
@@ -32,44 +32,51 @@ public function testValidateUnique() {
       'unique_user_textfield' => '{unique_user_textfield}',
       'unique_entity_textfield' => '{unique_entity_textfield}',
       'unique_error' => '{unique_error}',
+      'unique_multiple[1]' => TRUE,
     ];
 
     // Check post submission with default values does not trigger
     // unique errors.
     $sid = $this->postSubmission($webform, $edit);
-    $this->assertNoRaw('The value <em class="placeholder">{unique_textfield}</em> has already been submitted once for the <em class="placeholder">unique_textfield</em> element. You may have already submitted this webform, or you need to use a different value.</li>');;
-    $this->assertNoRaw('unique_textfield_multiple error message.');;
+    $this->assertNoRaw('The value <em class="placeholder">{unique_textfield}</em> has already been submitted once for the <em class="placeholder">unique_textfield</em> element. You may have already submitted this webform, or you need to use a different value.</li>');
+    $this->assertNoRaw('unique_textfield_multiple error message.');
     $this->assertNoRaw('unique_user_textfield error message.');
     $this->assertNoRaw('unique_entity_textfield error message.');
     $this->assertNoRaw('unique_error error message.');
-    $this->assertNoRaw('unique_ignored error message.');
+    $this->assertNoRaw('unique_multiple error message.');
 
     // Check post duplicate submission with default values does trigger
     // unique errors.
     $this->postSubmission($webform, $edit);
-    $this->assertRaw('The value <em class="placeholder">{unique_textfield}</em> has already been submitted once for the <em class="placeholder">unique_textfield</em> element. You may have already submitted this webform, or you need to use a different value.</li>');;
-    $this->assertRaw('unique_textfield_multiple error message.');;
+    $this->assertRaw('The value <em class="placeholder">{unique_textfield}</em> has already been submitted once for the <em class="placeholder">unique_textfield</em> element. You may have already submitted this webform, or you need to use a different value.</li>');
+    $this->assertRaw('unique_textfield_multiple error message.');
     $this->assertRaw('unique_user_textfield error message.');
     $this->assertRaw('unique_entity_textfield error message.');
     $this->assertRaw('unique_error error message.');
-    $this->assertNoRaw('unique_ignored error message.');
+    $this->assertRaw('unique_multiple error message.');
 
     // Check #unique element can be updated.
     $this->drupalPostForm("admin/structure/webform/manage/test_element_validate_unique/submission/$sid/edit", [], t('Save'));
-    $this->assertNoRaw('The value <em class="placeholder">{unique_textfield}</em> has already been submitted once for the <em class="placeholder">unique_textfield</em> element. You may have already submitted this webform, or you need to use a different value.</li>');;
+    $this->assertNoRaw('The value <em class="placeholder">{unique_textfield}</em> has already been submitted once for the <em class="placeholder">unique_textfield</em> element. You may have already submitted this webform, or you need to use a different value.</li>');
     $this->assertNoRaw('unique_user_textfield error message.');
     $this->assertNoRaw('unique_entity_textfield error message.');
     $this->assertNoRaw('unique_error error message.');
-    $this->assertNoRaw('unique_ignored error message.');
+    $this->assertNoRaw('unique_multiple error message.');
 
     // Check #unique multiple validation within the same element.
     // @see \Drupal\webform\Plugin\WebformElementBase::validateUniqueMultiple
+    // Add 2 more items.
+    $edit = [
+      'unique_textfield_multiple[add][more_items]' => 2,
+    ];
+    $this->drupalPostAjaxForm('webform/test_element_validate_unique', $edit, 'unique_textfield_multiple_table_add');
+
     $edit = [
       'unique_textfield_multiple[items][0][_item_]' => '{same}',
       'unique_textfield_multiple[items][2][_item_]' => '{same}',
     ];
-    $this->postSubmission($webform, $edit);
-    $this->assertRaw('unique_textfield_multiple error message.');;
+    $this->drupalPostForm(NULL, $edit, t('Submit'));
+    $this->assertRaw('unique_textfield_multiple error message.');
 
     // Purge existing submissions.
     $this->purgeSubmissions();
diff --git a/web/modules/webform/src/Tests/Exporter/WebformExporterExcludedTest.php b/web/modules/webform/src/Tests/Exporter/WebformExporterExcludedTest.php
index 190231cc94262919588627e50999bd630fe9baf1..82a3bd3746f01d6468cc8edd37eb6864c1e818ed 100644
--- a/web/modules/webform/src/Tests/Exporter/WebformExporterExcludedTest.php
+++ b/web/modules/webform/src/Tests/Exporter/WebformExporterExcludedTest.php
@@ -18,7 +18,7 @@ public function testExcludeExporters() {
     $this->drupalLogin($this->rootUser);
 
     // Check exporter options.
-    $this->drupalGet('admin/structure/webform/manage/contact/results/download');
+    $this->drupalGet('/admin/structure/webform/manage/contact/results/download');
     $this->assertRaw('<option value="delimited"');
     $this->assertRaw('<option value="table"');
     $this->assertRaw('<option value="json"');
@@ -28,7 +28,7 @@ public function testExcludeExporters() {
     \Drupal::configFactory()->getEditable('webform.settings')->set('export.excluded_exporters', ['delimited' => 'delimited'])->save();
 
     // Check delimited exporter excluded.
-    $this->drupalGet('admin/structure/webform/manage/contact/results/download');
+    $this->drupalGet('/admin/structure/webform/manage/contact/results/download');
     $this->assertNoRaw('<option value="delimited"');
     $this->assertRaw('<option value="table"');
     $this->assertRaw('<option value="json"');
diff --git a/web/modules/webform/src/Tests/Form/WebformFormPropertiesTest.php b/web/modules/webform/src/Tests/Form/WebformFormPropertiesTest.php
index f71555f50a429633ab0736d2df594b5e14ff08c3..9f42fd5af319b135201f90ccfdb122270360b021 100644
--- a/web/modules/webform/src/Tests/Form/WebformFormPropertiesTest.php
+++ b/web/modules/webform/src/Tests/Form/WebformFormPropertiesTest.php
@@ -25,11 +25,11 @@ public function testProperties() {
     global $base_path;
 
     // Check invalid elements .
-    $this->drupalGet('webform/test_element_invalid');
+    $this->drupalGet('/webform/test_element_invalid');
     $this->assertRaw('Unable to display this webform. Please contact the site administrator.');
 
     // Check element's root properties moved to the webform's properties.
-    $this->drupalGet('webform/test_form_properties');
+    $this->drupalGet('/webform/test_form_properties');
     $this->assertPattern('/Form prefix<form /');
     $this->assertPattern('/<\/form>\s+Form suffix/');
     $this->assertRaw('<form class="webform-submission-form webform-submission-add-form webform-submission-test-form-properties-form webform-submission-test-form-properties-add-form test-form-properties js-webform-details-toggle webform-details-toggle" invalid="invalid" style="border: 10px solid red; padding: 1em;" data-drupal-selector="webform-submission-test-form-properties-add-form" action="https://www.google.com/search" method="get" id="webform-submission-test-form-properties-add-form" accept-charset="UTF-8">');
@@ -48,7 +48,7 @@ public function testProperties() {
 'prefix': 'Form prefix TEST'",
     ];
     $this->drupalPostForm('/admin/structure/webform/manage/test_form_properties/settings/form', $edit, t('Save'));
-    $this->drupalGet('webform/test_form_properties');
+    $this->drupalGet('/webform/test_form_properties');
     $this->assertPattern('/Form prefix TEST<form /');
     $this->assertPattern('/<\/form>\s+Form suffix TEST/');
     $this->assertRaw('<form class="webform-submission-form webform-submission-add-form webform-submission-test-form-properties-form webform-submission-test-form-properties-add-form form--inline clearfix test-form-properties js-webform-details-toggle webform-details-toggle" style="border: 10px solid green; padding: 1em;" data-drupal-selector="webform-submission-test-form-properties-add-form" action="' . $base_path . 'webform/test_form_properties" method="post" id="webform-submission-test-form-properties-add-form" accept-charset="UTF-8">');
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerActionTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerActionTest.php
index 76afec76542f3d31280b649aff6ddf70ab8e0786..dd639a3cba6c659ca44e6c829bb55ee8997f1dbd 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerActionTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerActionTest.php
@@ -8,7 +8,7 @@
 use Drupal\webform\Tests\WebformTestBase;
 
 /**
- * Tests for action  webform handler functionality.
+ * Tests for action webform handler functionality.
  *
  * @group Webform
  */
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerConditionsTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerConditionsTest.php
index cbff0d21875fb815a9f44036b873cfe22123904c..a046433f416c768aedb4c04b732329cdb35b4883 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerConditionsTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerConditionsTest.php
@@ -35,7 +35,7 @@ public function testConditions() {
     /** @var \Drupal\webform\WebformInterface $webform */
     $webform = Webform::load('test_handler_conditions');
 
-    $this->drupalGet('webform/test_handler_conditions');
+    $this->drupalGet('/webform/test_handler_conditions');
 
     // Check no triggers.
     $this->assertRaw('Invoked test_a: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:preCreate');
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailAdvancedTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailAdvancedTest.php
index 2ba78064d97fd9d0c9802801d1337e674580605f..7a906eb9424543cb46db0d7bc0167ed9fc896dd0 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailAdvancedTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailAdvancedTest.php
@@ -27,26 +27,8 @@ class WebformHandlerEmailAdvancedTest extends WebformTestBase {
   public function setUp() {
     parent::setUp();
 
-    // Create users.
-    $this->createUsers();
-  }
-
-  /**
-   * Create webform test users.
-   */
-  protected function createUsers() {
     // Create filter.
     $this->createFilters();
-
-    $this->normalUser = $this->drupalCreateUser([
-      'access user profiles',
-      $this->basicHtmlFilter->getPermissionName(),
-    ]);
-    $this->adminSubmissionUser = $this->drupalCreateUser([
-      'access user profiles',
-      'administer webform submission',
-      $this->basicHtmlFilter->getPermissionName(),
-    ]);
   }
 
   /**
@@ -59,9 +41,13 @@ protected function createUsers() {
    * @see \Drupal\Core\Mail\Plugin\Mail\TestMailCollector
    */
   public function testAdvancedEmailHandler() {
+    global $base_url;
+
     /** @var \Drupal\webform\WebformInterface $webform */
     $webform = Webform::load('test_handler_email_advanced');
 
+    /**************************************************************************/
+
     // Generate a test submission with a file upload.
     $this->drupalLogin($this->rootUser);
 
@@ -136,7 +122,7 @@ public function testAdvancedEmailHandler() {
       // @see http://cgit.drupalcode.org/drupal/tree/core/lib/Drupal/Core/Mail/MailManager.php#n285
       'subject' => 'This has <removed>"special" \'chararacters\'',
       'message[value]' => '<p><em>Please enter a message.</em> Test that double "quotes" are not encoded.</p>',
-      'optional' => '',
+      'checkbox' => FALSE,
     ];
     $this->postSubmissionTest($webform, $edit);
     $sid = $this->getLastSubmissionId($webform);
@@ -146,13 +132,14 @@ public function testAdvancedEmailHandler() {
     $this->assertEqual($sent_email['subject'], 'This has "special" \'chararacters\'');
 
     // Check email body is HTML.
-    $this->assertContains($sent_email['params']['body'], '<b>First name</b><br />John<br /><br />');
-    $this->assertContains($sent_email['params']['body'], '<b>Last name</b><br />Smith<br /><br />');
-    $this->assertContains($sent_email['params']['body'], '<b>Email</b><br /><a href="mailto:from@example.com">from@example.com</a><br /><br />');
-    $this->assertContains($sent_email['params']['body'], '<b>Subject</b><br />This has &lt;removed&gt;&quot;special&quot; &#039;chararacters&#039;<br /><br />');
-    $this->assertContains($sent_email['params']['body'], '<b>Message</b><br /><p><em>Please enter a message.</em> Test that double "quotes" are not encoded.</p><br /><br />');
-    $this->assertContains($sent_email['params']['body'], '<p style="color:yellow"><em>Custom styled HTML markup</em></p>');
-    $this->assertNotContains($sent_email['params']['body'], '<b>Optional</b><br />{Empty}<br /><br />');
+    $this->assertContains('<b>First name</b><br />John<br /><br />', $sent_email['params']['body']);
+    $this->assertContains('<b>Last name</b><br />Smith<br /><br />', $sent_email['params']['body']);
+    $this->assertContains('<b>Email</b><br /><a href="mailto:from@example.com">from@example.com</a><br /><br />', $sent_email['params']['body']);
+    $this->assertContains('<b>Subject</b><br />This has &lt;removed&gt;&quot;special&quot; &#039;chararacters&#039;<br /><br />', $sent_email['params']['body']);
+    $this->assertContains('<b>Message</b><br /><p><em>Please enter a message.</em> Test that double "quotes" are not encoded.</p><br /><br />', $sent_email['params']['body']);
+    $this->assertContains('<p style="color:yellow"><em>Custom styled HTML markup</em></p>', $sent_email['params']['body']);
+    $this->assertNotContains('<b>Optional</b><br />{Empty}<br /><br />', $sent_email['params']['body']);
+    $this->assertNotContains('<b>Checkbox/b><br />Yes<br /><br />', $sent_email['params']['body']);
 
     // Check email has attachment.
     $this->assertEqual($sent_email['params']['attachments'][0]['filecontent'], "this is a sample txt file\nit has two lines\n");
@@ -161,14 +148,14 @@ public function testAdvancedEmailHandler() {
 
     // Check resend webform includes link to the attachment.
     $this->drupalGet("admin/structure/webform/manage/test_handler_email_advanced/submission/$sid/resend");
-    $this->assertRaw('<span class="file file--mime-text-plain file--text">');
-    $this->assertRaw('file.txt');
+    $this->assertRaw('<strong><a href="' . $base_url . '/system/files/webform/test_handler_email_advanced/6/file.txt">file.txt</a></strong> (text/plain) - 43 bytes');
 
     // Check resend webform with custom message.
-    $this->drupalPostForm("admin/structure/webform/manage/test_handler_email_advanced/submission/$sid/resend", ['message[body][value]' => 'Testing 123...'], t('Resend message'));
+    $this->drupalPostForm("admin/structure/webform/manage/test_handler_email_advanced/submission/$sid/resend", ['message[body][value]' => 'Testing 123…'], t('Resend message'));
     $sent_email = $this->getLastEmail();
-    $this->assertNotContains($sent_email['params']['body'], '<b>First name</b><br />John<br /><br />');
-    $this->assertEqual($sent_email['params']['body'], 'Testing 123...');
+    $this->assertNotContains('<b>First name</b><br />John<br /><br />', $sent_email['params']['body']);
+    $this->debug($sent_email['params']['body']);
+    $this->assertEqual($sent_email['params']['body'], 'Testing 123…');
 
     // Check resent email has the same attachment.
     $this->assertEqual($sent_email['params']['attachments'][0]['filecontent'], "this is a sample txt file\nit has two lines\n");
@@ -191,18 +178,20 @@ public function testAdvancedEmailHandler() {
     // Check empty element is excluded.
     $this->postSubmission($webform);
     $sent_email = $this->getLastEmail();
-    $this->assertNotContains($sent_email['params']['body'], '<b>Optional</b><br />{Empty}<br /><br />');
+    $this->assertNotContains('<b>Optional</b><br />{Empty}<br /><br />', $sent_email['params']['body']);
 
     // Include empty.
     $configuration = $email_handler->getConfiguration();
     $configuration['settings']['exclude_empty'] = FALSE;
+    $configuration['settings']['exclude_empty_checkbox'] = FALSE;
     $email_handler->setConfiguration($configuration);
     $webform->save();
 
     // Check empty included.
     $this->postSubmission($webform);
     $sent_email = $this->getLastEmail();
-    $this->assertContains($sent_email['params']['body'], '<b>Optional</b><br />{Empty}<br /><br />');
+    $this->assertContains('<b>Optional</b><br />{Empty}<br /><br />', $sent_email['params']['body']);
+    $this->assertContains('<b>Checkbox</b><br />No<br /><br />', $sent_email['params']['body']);
 
     // Logut and use anonymous user account.
     $this->drupalLogout();
@@ -210,7 +199,7 @@ public function testAdvancedEmailHandler() {
     // Check that private is include in email because 'ignore_access' is TRUE.
     $this->postSubmission($webform);
     $sent_email = $this->getLastEmail();
-    $this->assertContains($sent_email['params']['body'], '<b>Notes</b><br />These notes are private.<br /><br />');
+    $this->assertContains('<b>Notes</b><br />These notes are private.<br /><br />', $sent_email['params']['body']);
 
     // Disable ignore_access.
     $email_handler = $webform->getHandler('email');
@@ -222,7 +211,7 @@ public function testAdvancedEmailHandler() {
     // Check that private is excluded from email because 'ignore_access' is FALSE.
     $this->postSubmission($webform);
     $sent_email = $this->getLastEmail();
-    $this->assertNotContains($sent_email['params']['body'], '<b>Notes</b><br />These notes are private.<br /><br />');
+    $this->assertNotContains('<b>Notes</b><br />These notes are private.<br /><br />', $sent_email['params']['body']);
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailBasicTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailBasicTest.php
index 2da586786dde281b0b4f19d59d4f77ddb0b2bd14..fd54eaee2296719118e075833bae9d475c23088c 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailBasicTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailBasicTest.php
@@ -21,23 +21,19 @@ class WebformHandlerEmailBasicTest extends WebformTestBase {
    */
   protected static $testWebforms = ['test_handler_email'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
   /**
    * Test basic email handler.
    */
   public function testBasicEmailHandler() {
+    $admin_user = $this->drupalCreateUser([
+      'administer webform',
+    ]);
+
     /** @var \Drupal\webform\WebformInterface $webform */
     $webform = Webform::load('test_handler_email');
 
+    /**************************************************************************/
+
     // Create a submission using the test webform's default values.
     $this->postSubmission($webform);
 
@@ -45,9 +41,9 @@ public function testBasicEmailHandler() {
     $sent_email = $this->getLastEmail();
     $this->assertEqual($sent_email['key'], 'test_handler_email_email');
     $this->assertEqual($sent_email['reply-to'], "John Smith <from@example.com>");
-    $this->assertContains($sent_email['body'], 'Submitted by: Anonymous');
-    $this->assertContains($sent_email['body'], 'First name: John');
-    $this->assertContains($sent_email['body'], 'Last name: Smith');
+    $this->assertContains('Submitted by: Anonymous', $sent_email['body']);
+    $this->assertContains('First name: John', $sent_email['body']);
+    $this->assertContains('Last name: Smith', $sent_email['body']);
     $this->assertEqual($sent_email['headers']['From'], 'John Smith <from@example.com>');
     $this->assertEqual($sent_email['headers']['Cc'], 'cc@example.com');
     $this->assertEqual($sent_email['headers']['Bcc'], 'bcc@example.com');
@@ -60,12 +56,12 @@ public function testBasicEmailHandler() {
     $webform->setSetting('results_disabled', TRUE)->save();
     $this->postSubmission($webform, ['first_name' => 'Jane', 'last_name' => 'Doe']);
     $sent_email = $this->getLastEmail();
-    $this->assertContains($sent_email['body'], 'First name: Jane');
-    $this->assertContains($sent_email['body'], 'Last name: Doe');
+    $this->assertContains('First name: Jane', $sent_email['body']);
+    $this->assertContains('Last name: Doe', $sent_email['body']);
     $webform->setSetting('results_disabled', FALSE)->save();
 
     // Check sending a custom email using tokens.
-    $this->drupalLogin($this->adminWebformUser);
+    $this->drupalLogin($admin_user);
     $body = implode(PHP_EOL, [
       'full name: [webform_submission:values:first_name] [webform_submission:values:last_name]',
       'uuid: [webform_submission:uuid]',
@@ -85,17 +81,20 @@ public function testBasicEmailHandler() {
     $webform_submission = WebformSubmission::load($sid);
 
     $sent_email = $this->getLastEmail();
-    $this->assertContains($sent_email['body'], 'full name: John Smith');
-    $this->assertContains($sent_email['body'], 'uuid: ' . $webform_submission->uuid->value);
-    $this->assertContains($sent_email['body'], 'sid: ' . $sid);
-    $this->assertContains($sent_email['body'], 'date: ' . \Drupal::service('date.formatter')->format($webform_submission->created->value, 'medium'));
-    $this->assertContains($sent_email['body'], 'ip-address: ' . $webform_submission->remote_addr->value);
-    $this->assertContains($sent_email['body'], 'user: ' . $this->adminWebformUser->label());
-    $this->assertContains($sent_email['body'], "url:");
-    $this->assertContains($sent_email['body'], $webform_submission->toUrl('canonical', ['absolute' => TRUE])->toString());
-    $this->assertContains($sent_email['body'], "edit-url:");
-    $this->assertContains($sent_email['body'], $webform_submission->toUrl('edit-form', ['absolute' => TRUE])->toString());
-    $this->assertContains($sent_email['body'], 'Test that "double quotes" are not encoded.');
+    $this->assertContains('full name: John Smith', $sent_email['body']);
+    $this->assertContains('uuid: ' . $webform_submission->uuid->value, $sent_email['body']);
+    $this->assertContains('sid: ' . $sid, $sent_email['body']);
+    $date_value = \Drupal::service('date.formatter')->format($webform_submission->created->value, 'medium');
+    $this->assertContains('date: ' . $date_value, $sent_email['body']);
+    $this->assertContains('ip-address: ' . $webform_submission->remote_addr->value, $sent_email['body']);
+    $this->assertContains('user: ' . $admin_user->label(), $sent_email['body']);
+    $this->assertContains("url:", $sent_email['body']);
+    $this->assertContains($webform_submission->toUrl('canonical', ['absolute' => TRUE])
+      ->toString(), $sent_email['body']);
+    $this->assertContains("edit-url:", $sent_email['body']);
+    $this->assertContains($webform_submission->toUrl('edit-form', ['absolute' => TRUE])
+      ->toString(), $sent_email['body']);
+    $this->assertContains('Test that "double quotes" are not encoded.', $sent_email['body']);
 
     // Create a submission using HTML is subject and message.
     $edit = [
@@ -117,7 +116,7 @@ public function testBasicEmailHandler() {
     ];
     $this->postSubmission($webform, $edit);
     $sent_email = $this->getLastEmail();
-    $this->assertEqual($sent_email['reply-to'], '"first_name" "last_name" <from@example.com>');
+    $this->assertEqual($sent_email['reply-to'], '"first_name\\" \\"last_name" <from@example.com>');
     $this->assertEqual($sent_email['subject'], 'This has  & "special" \'characters\'');
     // NOTE:
     // Drupal's PhpMail::format function calls
@@ -135,8 +134,7 @@ public function testBasicEmailHandler() {
 This has  & "special" \'characters\'
 
 This has  & "special" \'characters\'
-'
-);
+');
     // Instead we are going to check params body.
     $this->assertEqual($sent_email['params']['body'], 'First name: "<first_name>"
 Last name: "<last_name>"
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailMappingTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailMappingTest.php
index be6aa3041579bd9630f44c2433d194b4f9c56381..7165b4433be91e0d858e5c6eaa6ad1a5fbf95aa0 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailMappingTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailMappingTest.php
@@ -60,7 +60,7 @@ public function testEmailMapping() {
     $this->assertText("Checkboxes sent to saturday@example.com,sunday@example.com from $site_name [$site_mail].");
     $this->assertNoText('Email not sent for Checkboxes handler because a To, CC, or BCC email was not provided.');
 
-    // Check that checkbxoes other option email sent.
+    // Check that checkboxes other option email sent.
     $edit = [
       'radios_other[radios]' => '_other_',
       'radios_other[other]' => '{Other}',
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailRenderingTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailRenderingTest.php
index 9fb342f2314eb890c5f5ad13be0a03cfd03ccbe4..e47d267595ca77fe56c05a0003934a42af4cd0c9 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailRenderingTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailRenderingTest.php
@@ -37,7 +37,7 @@ public function testEmailRendering() {
     $webform = Webform::load('contact');
 
     // Check that we are currently using the bartik.theme.
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertRaw('core/themes/bartik/css/base/elements.css');
 
     // Post submission and send emails.
@@ -68,7 +68,7 @@ public function testEmailRendering() {
     $webform->save();
 
     // Check that we are now using the seven.theme.
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertNoRaw('core/themes/bartik/css/base/elements.css');
 
     // Post submission and send emails.
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailRolesTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailRolesTest.php
index 0cdd57a9f3dec130c80168124772a4b0c17d4330..db8ae40956697c627368850b67deebdf53a87049 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailRolesTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailRolesTest.php
@@ -33,20 +33,20 @@ public function testEmailRoles() {
     // creates 'administrator' role.
     // WORKAROUND: Create 'administrator' role so that SimpleTest and Drupal
     // are in-sync.
-    $this->createRole([], 'administrator');
+    $this->drupalCreateRole([], 'administrator');
 
     $webform = Webform::load('test_handler_email_roles');
 
-    $authenticated_user = $this->createUser();
+    $authenticated_user = $this->drupalCreateUser();
     $authenticated_user->set('mail', 'authenticated@example.com');
     $authenticated_user->save();
 
-    $blocked_user = $this->createUser();
+    $blocked_user = $this->drupalCreateUser();
     $blocked_user->set('mail', 'blocked@example.com');
     $blocked_user->block();
     $blocked_user->save();
 
-    $admin_user = $this->createUser();
+    $admin_user = $this->drupalCreateUser();
     $admin_user->set('mail', 'administrator@example.com');
     $admin_user->addRole('administrator');
     $admin_user->save();
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailStatesTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailStatesTest.php
index 5f1baccd132efce4edbf329607e7bde5e0f7b8af..81616110694bd712943b85baa9af811d5945e799 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailStatesTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailStatesTest.php
@@ -53,20 +53,25 @@ public function testEmailStates() {
     $this->assertRaw('<b>Subject:</b> Submission converted<br />');
     $this->assertRaw('<b>Subject:</b> Submission completed<br />');
     $this->assertRaw('<b>Subject:</b> Submission updated<br />');
+    $this->assertRaw('<b>Subject:</b> Submission locked<br />');
     $this->assertRaw('<b>Subject:</b> Submission deleted<br />');
     $this->assertRaw('<b>Subject:</b> Submission custom<br />');
 
+    // Check locked email.
+    $this->drupalPostForm("/admin/structure/webform/manage/test_handler_email_states/submission/$sid/notes", ['locked' => TRUE], t('Save'));
+    $this->assertRaw('Debug: Email: Submission locked');
+
     // Check deleted email.
     $this->drupalPostForm("/admin/structure/webform/manage/test_handler_email_states/submission/$sid/delete", [], t('Delete'));
     $this->assertRaw('Debug: Email: Submission deleted');
 
-    // Check that 'Send when...' is visible.
-    $this->drupalGet('admin/structure/webform/manage/test_handler_email_states/handlers/email_draft/edit');
+    // Check that 'Send when…' is visible.
+    $this->drupalGet('/admin/structure/webform/manage/test_handler_email_states/handlers/email_draft/edit');
     $this->assertRaw('<span class="fieldset-legend">Send email</span>');
 
     // Check states hidden when results are disabled.
     $webform->setSetting('results_disabled', TRUE)->save();
-    $this->drupalGet('admin/structure/webform/manage/test_handler_email_states/handlers/email_draft/edit');
+    $this->drupalGet('/admin/structure/webform/manage/test_handler_email_states/handlers/email_draft/edit');
     $this->assertNoRaw('<span class="fieldset-legend js-form-required form-required">Send email</span>');
 
     // Check that only completed email is triggered when states are disabled.
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailTwigTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailTwigTest.php
index 017fcf937d9c582a19a41726e7f307f5b8b53738..66fbe3c492f859ad447bb1e52a6df6857d756553 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerEmailTwigTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerEmailTwigTest.php
@@ -41,6 +41,7 @@ public function testEmailTwigHandler() {
   <b>Subject</b><br />{subject}<br /><br />
 
   <b>Message</b><br />{message}<br /><br />');
+
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerExcludedTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerExcludedTest.php
index a6a4c7bcf67c3a7fb9e44c52643241aa306fad19..489e103f20243119dc5caff455ad6795f4758cc6 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerExcludedTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerExcludedTest.php
@@ -11,6 +11,21 @@
  */
 class WebformHandlerExcludedTest extends WebformTestBase {
 
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['block', 'webform'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->drupalPlaceBlock('local_actions_block');
+  }
+
   /**
    * Test excluded handlers.
    */
@@ -21,31 +36,31 @@ public function testExcludeHandlers() {
     $handler_manager = $this->container->get('plugin.manager.webform.handler');
 
     // Check add mail and handler plugin.
-    $this->drupalGet('admin/structure/webform/manage/contact/handlers');
+    $this->drupalGet('/admin/structure/webform/manage/contact/handlers');
     $this->assertLink('Add email');
     $this->assertLink('Add handler');
 
     // Check add mail accessible.
-    $this->drupalGet('admin/structure/webform/manage/contact/handlers/add/email');
+    $this->drupalGet('/admin/structure/webform/manage/contact/handlers/add/email');
     $this->assertResponse(200);
 
     // Exclude the email handler.
     \Drupal::configFactory()->getEditable('webform.settings')->set('handler.excluded_handlers', ['email' => 'email'])->save();
 
     // Check add mail hidden.
-    $this->drupalGet('admin/structure/webform/manage/contact/handlers');
+    $this->drupalGet('/admin/structure/webform/manage/contact/handlers');
     $this->assertNoLink('Add email');
     $this->assertLink('Add handler');
 
     // Check add mail access denied.
-    $this->drupalGet('admin/structure/webform/manage/contact/handlers/add/email');
+    $this->drupalGet('/admin/structure/webform/manage/contact/handlers/add/email');
     $this->assertResponse(403);
 
     // Exclude the email handler.
     \Drupal::configFactory()->getEditable('webform.settings')->set('handler.excluded_handlers', ['action' => 'action', 'broken' => 'broken', 'debug' => 'debug', 'email' => 'email', 'remote_post' => 'remote_post', 'settings' => 'settings'])->save();
 
     // Check add mail and handler hidden.
-    $this->drupalGet('admin/structure/webform/manage/contact/handlers');
+    $this->drupalGet('/admin/structure/webform/manage/contact/handlers');
     $this->assertNoLink('Add email');
     $this->assertNoLink('Add handler');
 
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerInvokeAlterHookTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerInvokeAlterHookTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..5f8d14890fd04ec26a84074c1cd5d5af9ec258d5
--- /dev/null
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerInvokeAlterHookTest.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\webform\Tests\Handler;
+
+use Drupal\webform\Tests\WebformTestBase;
+
+/**
+ * Tests for the webform handler invoke alter hook.
+ *
+ * @group Webform
+ */
+class WebformHandlerInvokeAlterHookTest extends WebformTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'webform_test_handler_invoke_alter'];
+
+  /**
+   * Tests webform handler invoke alter hook.
+   */
+  public function testWebformHandlerInvokeAlterHook() {
+    // Check invoke alter hooks.
+    $this->drupalGet('/webform/contact');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_alter() for "contact:email_confirmation::pre_create"');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_pre_create_alter() for "contact:email_confirmation"');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_alter() for "contact:email_notification::pre_create"');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_pre_create_alter() for "contact:email_notification"');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_alter() for "contact:email_confirmation::alter_elements"');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_alter() for "contact:email_notification::alter_elements"');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_alter() for "contact:email_confirmation::alter_elements"');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_alter() for "contact:email_notification::alter_elements"');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_alter() for "contact:email_confirmation::post_create"');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_alter() for "contact:email_notification::post_create"');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_alter() for "contact:email_confirmation::override_settings"');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_alter() for "contact:email_notification::override_settings"');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_alter() for "contact:email_confirmation::alter_form"');
+    $this->assertRaw('Invoking hook_webform_handler_invoke_alter() for "contact:email_notification::alter_form"');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerPluginTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerPluginTest.php
index e1e7eac1a7ac0b0b611af3a731008140046e7ab6..8a5d861da45cae3ac6cf674a164b23e28bc4326e 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerPluginTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerPluginTest.php
@@ -20,7 +20,7 @@ class WebformHandlerPluginTest extends WebformTestBase {
   public static $modules = ['webform', 'webform_test_handler'];
 
   /**
-   * Tests webform element plugin.
+   * Tests webform handler plugin.
    */
   public function testWebformHandler() {
     $webform = Webform::load('contact');
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerRemotePostTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerRemotePostTest.php
index a8dbcf0178d70252fb823865e3c82fa065202449..213fee20b7a674d8ce19b56078df5dd7c06cb25a 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerRemotePostTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerRemotePostTest.php
@@ -28,6 +28,7 @@ class WebformHandlerRemotePostTest extends WebformTestBase {
    */
   protected static $testWebforms = [
     'test_handler_remote_post',
+    'test_handler_remote_put',
     'test_handler_remote_get',
     'test_handler_remote_post_file',
   ];
@@ -47,6 +48,22 @@ public function testRemotePostHandler() {
 
     // Check 'completed' operation.
     $sid = $this->postSubmission($webform);
+
+    // Check POST response.
+    $this->assertRaw("method: post
+status: success
+message: 'Processed completed request.'
+options:
+  headers:
+    Accept-Language: en
+    custom_header: 'true'
+  form_params:
+    custom_completed: true
+    custom_data: true
+    response_type: '200'
+    first_name: John
+    last_name: Smith");
+
     $webform_submission = WebformSubmission::load($sid);
     $this->assertRaw("form_params:
   custom_completed: true
@@ -61,7 +78,7 @@ public function testRemotePostHandler() {
     $this->assertRaw('Your confirmation number is ' . $webform_submission->getElementData('confirmation_number') . '.');
 
     // Check custom header.
-    $this->assertRaw('{&quot;custom_header&quot;:&quot;true&quot;}');
+    $this->assertRaw('{&quot;headers&quot;:{&quot;Accept-Language&quot;:&quot;en&quot;,&quot;custom_header&quot;:&quot;true&quot;}');
 
     // Sleep for 1 second to make sure submission timestamp is updated.
     sleep(1);
@@ -166,6 +183,29 @@ public function testRemotePostHandler() {
     preg_match('/&quot;confirmation_number&quot;:&quot;([a-zA-z0-9]+)&quot;/', $this->getRawContent(), $match);
     $this->assertRaw('Your confirmation number is ' . $match[1] . '.');
 
+    /**************************************************************************/
+    // PUT.
+    /**************************************************************************/
+
+    /** @var \Drupal\webform\WebformInterface $webform */
+    $webform = Webform::load('test_handler_remote_put');
+
+    $this->postSubmission($webform);
+
+    // Check PUT response.
+    $this->assertRaw("method: put
+status: success
+message: 'Processed completed request.'
+options:
+  headers:
+    custom_header: 'true'
+  form_params:
+    custom_completed: true
+    custom_data: true
+    response_type: '200'
+    first_name: John
+    last_name: Smith");
+
     /**************************************************************************/
     // GET.
     /**************************************************************************/
@@ -175,11 +215,20 @@ public function testRemotePostHandler() {
 
     $this->postSubmission($webform);
 
+    // Check GET response.
+    $this->assertRaw("method: get
+status: success
+message: 'Processed completed request.'
+options:
+  headers:
+    custom_header: 'true'");
+
     // Check request URL contains query string.
-    $this->assertRaw("http://webform-test-handler-remote-post/completed?custom_completed=1&amp;custom_data=1&amp;response_type=200&amp;first_name=John&amp;last_name=Smith");
+    // @todo restore tests once the Drupal 8.6.x issue is resolved.
+    //$this->assertRaw("http://webform-test-handler-remote-post/completed?custom_completed=1&amp;custom_data=1&amp;first_name=John&amp;last_name=Smith&amp;response_type=200");
 
     // Check response data.
-    $this->assertRaw("message: 'Processed completed?custom_completed=1&amp;custom_data=1&amp;response_type=200&amp;first_name=John&amp;last_name=Smith request.'");
+    $this->assertRaw("message: 'Processed completed request.'");
 
     // Get confirmation number from JSON packet.
     preg_match('/&quot;confirmation_number&quot;:&quot;([a-zA-z0-9]+)&quot;/', $this->getRawContent(), $match);
@@ -194,14 +243,43 @@ public function testRemotePostHandler() {
 
     $sid = $this->postSubmissionTest($webform);
     $webform_submission = WebformSubmission::load($sid);
-    $fid = $webform_submission->getElementData('file');
+
+    $file_data = $webform_submission->getElementData('file');
+    $file = File::load($file_data);
+    $file_id = $file->id();
+    $file_uuid = $file->uuid();
+
+    $files_data = $webform_submission->getElementData('files');
+    $file = File::load(reset($files_data));
+    $files_id = $file->id();
+    $files_uuid = $file->uuid();
 
     // Check the file name, uri, and data is appended to form params.
     $this->assertRaw("form_params:
-  file: $fid
+  file: 1
+  files:
+    - 2
+  _file:
+    id: $file_id
+    name: file.txt
+    uri: 'private://webform/test_handler_remote_post_file/$sid/file.txt'
+    mime: text/plain
+    uuid: $file_uuid
+    data: dGhpcyBpcyBhIHNhbXBsZSB0eHQgZmlsZQppdCBoYXMgdHdvIGxpbmVzCg==
+  file__id: $file_id
   file__name: file.txt
   file__uri: 'private://webform/test_handler_remote_post_file/$sid/file.txt'
-  file__data: dGhpcyBpcyBhIHNhbXBsZSB0eHQgZmlsZQppdCBoYXMgdHdvIGxpbmVzCg==");
+  file__mime: text/plain
+  file__uuid: $file_uuid
+  file__data: dGhpcyBpcyBhIHNhbXBsZSB0eHQgZmlsZQppdCBoYXMgdHdvIGxpbmVzCg==
+  _files:
+    -
+      id: $files_id
+      name: files.txt
+      uri: 'private://webform/test_handler_remote_post_file/$sid/files.txt'
+      mime: text/plain
+      uuid: $files_uuid
+      data: dGhpcyBpcyBhIHNhbXBsZSB0eHQgZmlsZQppdCBoYXMgdHdvIGxpbmVzCg==");
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerSettingsTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerSettingsTest.php
index d17f3c2362993bab042143e52d5d547118a2c499..dfda99cff01d2125cc8aaff6f03fec21a48a3c0a 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerSettingsTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerSettingsTest.php
@@ -36,7 +36,7 @@ public function testSettingsHandler() {
     $this->assertRaw($message_indentation . '{Custom draft saved message}');
 
     // Check custom save load message.
-    $this->drupalGet('webform/test_handler_settings');
+    $this->drupalGet('/webform/test_handler_settings');
     // NOTE: Adding indentation to make sure the message is matched and not input value.
     $this->assertRaw($message_indentation . '{Custom draft loaded message}');
 
@@ -60,7 +60,7 @@ public function testSettingsHandler() {
     $this->assertNoRaw($message_indentation . '{Custom draft saved message}');
 
     // Check no custom save load message.
-    $this->drupalGet('webform/test_handler_settings');
+    $this->drupalGet('/webform/test_handler_settings');
     $this->assertNoRaw($message_indentation . '{Custom draft loaded message}');
 
     // Check no custom preview title and message.
diff --git a/web/modules/webform/src/Tests/Handler/WebformHandlerTest.php b/web/modules/webform/src/Tests/Handler/WebformHandlerTest.php
index 4ae5b1e15d966d7287e82b9e47000102572e4df2..1a5c8317e7618c6bb38d3e581894bcc7ffccd1bb 100644
--- a/web/modules/webform/src/Tests/Handler/WebformHandlerTest.php
+++ b/web/modules/webform/src/Tests/Handler/WebformHandlerTest.php
@@ -38,7 +38,7 @@ public function testWebformHandler() {
     $webform_handler_test = Webform::load('test_handler_test');
 
     // Check new submission plugin invoking.
-    $this->drupalGet('webform/test_handler_test');
+    $this->drupalGet('/webform/test_handler_test');
     $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:preCreate');
     $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:postCreate');
     $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:alterElements');
@@ -76,7 +76,7 @@ public function testWebformHandler() {
     $this->assertRaw('<div class="webform-confirmation__message">::preprocessConfirmation</div>');
 
     // Check confirmation with token.
-    $this->drupalGet('webform/test_handler_test/confirmation', ['query' => ['token' => $webform_submission->getToken()]]);
+    $this->drupalGet('/webform/test_handler_test/confirmation', ['query' => ['token' => $webform_submission->getToken()]]);
     $this->assertNoRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:preSave');
     $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:postLoad');
     $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:alterElements');
@@ -85,7 +85,7 @@ public function testWebformHandler() {
     $this->assertRaw('<div class="webform-confirmation__message">::preprocessConfirmation</div>');
 
     // Check confirmation with out token.
-    $this->drupalGet('webform/test_handler_test/confirmation');
+    $this->drupalGet('/webform/test_handler_test/confirmation');
     $this->assertNoRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:postLoad');
     $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:alterElements');
     $this->assertNoRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:overrideSettings');
@@ -101,7 +101,7 @@ public function testWebformHandler() {
     $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:postLoad');
     $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:preDelete');
     $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:postDelete');
-    $this->assertRaw('Submission #' . $webform_submission->serial() . ' has been deleted.');
+    $this->assertRaw('<em class="placeholder">Test: Handler: Test invoke methods: Submission #' . $webform_submission->serial() . '</em> has been deleted.');
 
     // Check configuration settings.
     $this->drupalPostForm('admin/structure/webform/manage/test_handler_test/handlers/test/edit', ['settings[message]' => '{message}'], t('Save'));
@@ -110,7 +110,7 @@ public function testWebformHandler() {
 
     // Check disabling a handler.
     $this->drupalPostForm('admin/structure/webform/manage/test_handler_test/handlers/test/edit', ['status' => FALSE], t('Save'));
-    $this->drupalGet('webform/test_handler_test');
+    $this->drupalGet('/webform/test_handler_test');
     $this->assertNoRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:preCreate');
     $this->assertNoRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:postCreate');
     $this->assertNoRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:alterElements');
@@ -124,7 +124,7 @@ public function testWebformHandler() {
     // Check webform disabled with saving of results is disabled and handler does
     // not process results.
     $this->drupalLogout();
-    $this->drupalGet('webform/test_handler_test');
+    $this->drupalGet('/webform/test_handler_test');
     $this->assertNoFieldByName('op', 'Submit');
     $this->assertNoRaw('This webform is not saving or handling any submissions. All submitted data will be lost.');
     $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:preCreate');
@@ -134,7 +134,7 @@ public function testWebformHandler() {
 
     // Check admin can still post submission.
     $this->drupalLogin($this->rootUser);
-    $this->drupalGet('webform/test_handler_test');
+    $this->drupalGet('/webform/test_handler_test');
     $this->assertFieldByName('op', 'Submit');
     $this->assertRaw('This webform is currently not saving any submitted data.');
     $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:preCreate');
@@ -173,11 +173,44 @@ public function testWebformHandler() {
     $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:deleteHandler');
 
     // Check create handler.
-    $this->drupalPostForm('admin/structure/webform/manage/test_handler_test/handlers/add/test_log', ['handler_id' => 'test'], t('Save'));
+    $this->drupalPostForm('admin/structure/webform/manage/test_handler_test/handlers/add/test', ['handler_id' => 'test'], t('Save'));
     $this->assertRaw('The webform handler was successfully added.');
-    // @todo Determine why create message is not being displayed.
-    // Ajax machine name callback could be causing the issue.
-    // $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:createHandler');
+    $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:createHandler');
+
+    /**************************************************************************/
+    // Single handler.
+    /**************************************************************************/
+
+    // Check test handler is executed.
+    $this->drupalGet('/webform/test_handler_test/test');
+    $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:preCreate');
+    $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:postCreate');
+    $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:alterElements');
+    $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:overrideSettings');
+    $this->assertRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:alterForm');
+
+    // Check test handler is enabled and debug handler is disabled.
+    $this->drupalPostForm('webform/test_handler_test/test', ['element' => ''], t('Submit'));
+    $this->assertRaw('One two one two this is just a test');
+    $this->assertNoRaw("element: ''");
+
+    // Check test handler is disabled.
+    $this->drupalGet('/webform/test_handler_test/test', ['query' => ['_webform_handler' => 'debug']]);
+    $this->assertNoRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:preCreate');
+    $this->assertNoRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:postCreate');
+    $this->assertNoRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:alterElements');
+    $this->assertNoRaw('Invoked test: Drupal\webform_test_handler\Plugin\WebformHandler\TestWebformHandler:overrideSettings');
+    $this->assertRaw('Testing the <em class="placeholder">Test: Handler: Test invoke methods</em> webform <em class="placeholder">Debug</em> handler. <strong>All other emails/handlers are disabled.</strong>');
+
+    // Check test handler is now disabled and debug handler is enabled.
+    $this->drupalPostForm('webform/test_handler_test/test', ['element' => ''], t('Submit'), ['query' => ['_webform_handler' => 'debug']]);
+    $this->assertNoRaw('One two one two this is just a test');
+    $this->assertRaw("element: ''");
+
+    // Check 403 access denied for missing handler.
+    $this->drupalGet('/webform/test_handler_test/test', ['query' => ['_webform_handler' => 'missing']]);
+    $this->assertResponse(403);
+    $this->assertRaw('The <em class="placeholder">missing</em> email/handler for the <em class="placeholder">Test: Handler: Test invoke methods</em> webform does not exist.');
   }
 
   /**
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsAccessDeniedTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsAccessDeniedTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..cf8161a6f8824eb0011a8ac632d4edc28127d4b8
--- /dev/null
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsAccessDeniedTest.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace Drupal\webform\Tests\Settings;
+
+use Drupal\Core\Url;
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
+use Drupal\webform\WebformInterface;
+
+/**
+ * Tests for access denied webform and submissions.
+ *
+ * @group Webform
+ */
+class WebformSettingsAccessDeniedTest extends WebformTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['block', 'webform'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_form_access_denied'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Place blocks.
+    $this->placeBlocks();
+  }
+
+  /**
+   * Tests webform access denied setting.
+   */
+  public function testWebformAccessDenied() {
+    $webform = Webform::load('test_form_access_denied');
+    $webform_edit_route_url = Url::fromRoute('entity.webform.edit_form', [
+      'webform' => $webform->id(),
+    ]);
+
+    /**************************************************************************/
+    // Redirect.
+    /**************************************************************************/
+
+    // Set access denied to redirect with message.
+    $webform->setSetting('form_access_denied', WebformInterface::ACCESS_DENIED_LOGIN);
+    $webform->save();
+
+    // Check form message is displayed and user is redirected to the login form.
+    $this->drupalGet('/admin/structure/webform/manage/test_form_access_denied');
+    $this->assertRaw('Please login to access <b>Test: Webform: Access Denied</b>.');
+    $route_options = [
+      'query' => [
+        'destination' => $webform_edit_route_url->toString(),
+      ],
+    ];
+    $this->assertUrl(Url::fromRoute('user.login', [], $route_options));
+
+    /**************************************************************************/
+    // Default.
+    /**************************************************************************/
+
+    // Set default access denied page.
+    $webform->setSetting('form_access_denied', WebformInterface::ACCESS_DENIED_DEFAULT);
+    $webform->save();
+
+    // Check default access denied page.
+    $this->drupalGet('/admin/structure/webform/manage/test_form_access_denied');
+    $this->assertRaw('You are not authorized to access this page.');
+    $this->assertNoRaw('Please login to access <b>Test: Webform: Access Denied</b>.');
+
+    /**************************************************************************/
+    // Page.
+    /**************************************************************************/
+
+    // Set access denied to display a dedicated page.
+    $webform->setSetting('form_access_denied', WebformInterface::ACCESS_DENIED_PAGE);
+    $webform->setSetting('form_access_denied_title', 'Webform: Access denied');
+    $webform->setSetting('form_access_denied_attributes', ['style' => 'border: 1px solid red', 'class' => [], 'attributes' => []]);
+    $webform->save();
+
+    // Check custom access denied page.
+    $this->drupalGet('/admin/structure/webform/manage/test_form_access_denied');
+    $this->assertRaw('<h1 class="page-title">Webform: Access denied</h1>');
+    $this->assertRaw('<div style="border: 1px solid red" class="webform-access-denied">Please login to access <b>Test: Webform: Access Denied</b>.</div>');
+
+    /**************************************************************************/
+    // Message via a block.
+    /**************************************************************************/
+
+    // Place block.
+    $this->drupalPlaceBlock('webform_block', [
+      'webform_id' => 'test_form_access_denied',
+    ]);
+
+    // Set access denied to default.
+    $webform->setSetting('form_access_denied', WebformInterface::ACCESS_DENIED_DEFAULT);
+    $webform->save();
+
+    // Check block does not displays access denied message.
+    $this->drupalGet('/<front>');
+    $this->assertNoRaw('<div style="border: 1px solid red" class="webform-access-denied">Please login to access <b>Test: Webform: Access Denied</b>.</div>');
+
+    // Set access denied to display a message.
+    $webform->setSetting('form_access_denied', WebformInterface::ACCESS_DENIED_MESSAGE);
+    $webform->save();
+
+    // Check block displays access denied message.
+    $this->drupalGet('/<front>');
+    $this->assertRaw('<div style="border: 1px solid red" class="webform-access-denied">Please login to access <b>Test: Webform: Access Denied</b>.</div>');
+
+    // Login.
+    $this->drupalLogin($this->rootUser);
+
+    // Check block displays wth webform.
+    $this->drupalGet('/<front>');
+    $this->assertNoRaw('<div class="webform-access-denied">Please login to access <b>Test: Webform: Access Denied</b>.</div>');
+    $this->assertRaw('id="webform-submission-test-form-access-denied-user-1-add-form"');
+  }
+
+  /**
+   * Tests webform submission access denied setting.
+   */
+  public function testWebformSubmissionAccessDenied() {
+    // Create a webform submission.
+    $this->drupalLogin($this->rootUser);
+    $webform = Webform::load('test_form_access_denied');
+    $sid = $this->postSubmission($webform);
+    $this->drupalLogout();
+
+    /**************************************************************************/
+    // Redirect.
+    /**************************************************************************/
+
+    // Check submission message is displayed.
+    $this->drupalGet("admin/structure/webform/manage/test_form_access_denied/submission/$sid");
+    $this->assertRaw("Please login to access <b>Test: Webform: Access Denied: Submission #$sid</b>.");
+
+    /**************************************************************************/
+    // Default.
+    /**************************************************************************/
+
+    // Set default access denied page.
+    $webform->setSetting('submission_access_denied', WebformInterface::ACCESS_DENIED_DEFAULT);
+    $webform->save();
+
+    // Check default access denied page.
+    $this->drupalGet("admin/structure/webform/manage/test_form_access_denied/submission/$sid");
+    $this->assertRaw('You are not authorized to access this page.');
+    $this->assertNoRaw("Please login to access <b>Test: Webform: Access Denied: Submission #$sid</b>.");
+
+    /**************************************************************************/
+    // Page.
+    /**************************************************************************/
+
+    // Set access denied to display a dedicated page.
+    $webform->setSetting('submission_access_denied', WebformInterface::ACCESS_DENIED_PAGE);
+    $webform->setSetting('submission_access_denied_title', 'Webform submission: Access denied');
+    $webform->setSetting('submission_access_denied_attributes', ['style' => 'border: 1px solid red', 'class' => [], 'attributes' => []]);
+    $webform->save();
+
+    // Check custom access denied page.
+    $this->drupalGet("admin/structure/webform/manage/test_form_access_denied/submission/$sid");
+    $this->assertNoRaw('You are not authorized to access this page.');
+    $this->assertRaw('<h1 class="page-title">Webform submission: Access denied</h1>');
+    $this->assertRaw('<div style="border: 1px solid red" class="webform-submission-access-denied">Please login to access <b>Test: Webform: Access Denied: Submission #' . $sid . '</b>.</div>');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsAdminTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsAdminTest.php
index 017c1c430a8fd2f9bff7e4e6b92a4405eee77b1b..407e9189c6e32bf7a5f7ff03e0feaa90810105f3 100644
--- a/web/modules/webform/src/Tests/Settings/WebformSettingsAdminTest.php
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsAdminTest.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\webform\Tests\Settings;
 
-use Drupal\Component\Serialization\Yaml;
 use Drupal\webform\Tests\WebformTestBase;
 use Drupal\webform\Utility\WebformYaml;
 
@@ -18,7 +17,7 @@ class WebformSettingsAdminTest extends WebformTestBase {
    *
    * @var array
    */
-  public static $modules = ['node', 'webform', 'webform_ui'];
+  public static $modules = ['block', 'captcha', 'node', 'views', 'webform', 'webform_ui', 'webform_node'];
 
   /**
    * Webforms to load.
@@ -27,6 +26,14 @@ class WebformSettingsAdminTest extends WebformTestBase {
    */
   protected static $testWebforms = ['test_element'];
 
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->drupalPlaceBlock('local_actions_block');
+  }
+
   /**
    * Tests webform admin settings.
    */
@@ -60,8 +67,8 @@ public function testAdminSettings() {
       $this->assertEqual($updated_data, $original_data, 'Updated admin settings via the UI did not lose or change any data');
 
       // DEBUG:
-      $original_yaml = WebformYaml::tidy(Yaml::encode($original_data));
-      $updated_yaml = WebformYaml::tidy(Yaml::encode($updated_data));
+      $original_yaml = WebformYaml::encode($original_data);
+      $updated_yaml = WebformYaml::encode($updated_data);
       $this->verbose('<pre>' . $original_yaml . '</pre>');
       $this->verbose('<pre>' . $updated_yaml . '</pre>');
       $this->debug(array_diff(explode(PHP_EOL, $original_yaml), explode(PHP_EOL, $updated_yaml)));
@@ -70,40 +77,40 @@ public function testAdminSettings() {
     /* Elements */
 
     // Check that description is 'after' the element.
-    $this->drupalGet('webform/test_element');
+    $this->drupalGet('/webform/test_element');
     $this->assertPattern('#\{item title\}.+\{item markup\}.+\{item description\}#ms');
 
     // Set the default description display to 'before'.
     $this->drupalPostForm('admin/structure/webform/config/elements', ['element[default_description_display]' => 'before'], t('Save configuration'));
 
     // Check that description is 'before' the element.
-    $this->drupalGet('webform/test_element');
+    $this->drupalGet('/webform/test_element');
     $this->assertNoPattern('#\{item title\}.+\{item markup\}.+\{item description\}#ms');
     $this->assertPattern('#\{item title\}.+\{item description\}.+\{item markup\}#ms');
 
     /* UI disable dialog */
 
     // Check that dialogs are enabled.
-    $this->drupalGet('admin/structure/webform');
-    $this->assertRaw('<a href="' . $base_path . 'admin/structure/webform/add" class="button button-action button--primary button--small webform-ajax-link" data-dialog-type="modal" data-dialog-options="{&quot;width&quot;:700,&quot;dialogClass&quot;:&quot;webform-modal&quot;}">Add webform</a>');
+    $this->drupalGet('/admin/structure/webform');
+    $this->assertRaw('<a href="' . $base_path . 'admin/structure/webform/add" class="webform-ajax-link button button-action" data-dialog-type="modal" data-dialog-options="{&quot;width&quot;:700,&quot;dialogClass&quot;:&quot;webform-ui-dialog&quot;}" data-drupal-link-system-path="admin/structure/webform/add">Add webform</a>');
 
     // Disable dialogs.
     $this->drupalPostForm('admin/structure/webform/config/advanced', ['ui[dialog_disabled]' => TRUE], t('Save configuration'));
 
     // Check that dialogs are disabled. (i.e. use-ajax is not included)
-    $this->drupalGet('admin/structure/webform');
-    $this->assertNoRaw('<a href="' . $base_path . 'admin/structure/webform/add" class="button button-action button--primary button--small webform-ajax-link" data-dialog-type="modal" data-dialog-options="{&quot;width&quot;:700,&quot;dialogClass&quot;:&quot;webform-modal&quot;}">Add webform</a>');
-    $this->assertRaw('<a href="' . $base_path . 'admin/structure/webform/add" class="button button-action button--primary button--small">Add webform</a>');
+    $this->drupalGet('/admin/structure/webform');
+    $this->assertNoRaw('<a href="' . $base_path . 'admin/structure/webform/add" class="webform-ajax-link button button-action" data-dialog-type="modal" data-dialog-options="{&quot;width&quot;:700,&quot;dialogClass&quot;:&quot;webform-ui-dialog&quot;}" data-drupal-link-system-path="admin/structure/webform/add">Add webform</a>');
+    $this->assertRaw('<a href="' . $base_path . 'admin/structure/webform/add" class="button button-action" data-drupal-link-system-path="admin/structure/webform/add">Add webform</a>');
 
     /* UI description help */
 
     // Check moving #description to #help for webform admin routes.
     $this->drupalPostForm('admin/structure/webform/config/advanced', ['ui[description_help]' => TRUE], t('Save configuration'));
-    $this->assertRaw('<a href="#help" title="If checked, all element descriptions will be moved to help text (tooltip)." data-webform-help="If checked, all element descriptions will be moved to help text (tooltip)." class="webform-element-help">?</a>');
+    $this->assertRaw('<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;Display element description as help text (tooltip)&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;If checked, all element descriptions will be moved to help text (tooltip).&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
 
     // Check moving #description to #help for webform admin routes.
     $this->drupalPostForm('admin/structure/webform/config/advanced', ['ui[description_help]' => FALSE], t('Save configuration'));
-    $this->assertNoRaw('<a href="#help" title="If checked, all element descriptions will be moved to help text (tooltip)." data-webform-help="If checked, all element descriptions will be moved to help text (tooltip)." class="webform-element-help">?</a>');
+    $this->assertNoRaw('<span class="webform-element-help" role="tooltip" tabindex="0" data-webform-help="&lt;div class=&quot;webform-element-help--title&quot;&gt;Display element description as help text (tooltip)&lt;/div&gt;&lt;div class=&quot;webform-element-help--content&quot;&gt;If checked, all element descriptions will be moved to help text (tooltip).&lt;/div&gt;"><span aria-hidden="true">?</span></span>');
   }
 
   /**
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsArchivedTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsArchivedTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0a57da35b0e67034e207f2b5ec4583ac2fbe8f58
--- /dev/null
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsArchivedTest.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Drupal\webform\Tests\Settings;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
+
+/**
+ * Tests for webform archived.
+ *
+ * @group Webform
+ */
+class WebformSettingsArchivedTest extends WebformTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform_node', 'webform_templates', 'webform'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_form_archived'];
+
+  /**
+   * Test webform submission form archived.
+   */
+  public function testArchived() {
+    global $base_path;
+
+    $this->drupalLogin($this->rootUser);
+
+    $webform = Webform::load('test_form_archived');
+
+    // Check that archived webform is removed from webforms manage page.
+    $this->drupalGet('/admin/structure/webform');
+    $this->assertRaw('<td><a href="' . $base_path . 'form/contact">Contact</a></td>');
+    $this->assertNoRaw('<td><a href="' . $base_path . 'form/test-form-archived">Test: Webform: Archive</a></td>');
+
+    // Check that archived webform appears when archived filter selected.
+    $this->drupalGet('/admin/structure/webform', ['query' => ['state' => 'archived']]);
+    $this->assertNoRaw('<td><a href="' . $base_path . 'form/contact">Contact</a></td>');
+    $this->assertRaw('<td><a href="' . $base_path . 'form/test-form-archived">Test: Webform: Archive</a></td>');
+
+    // Check that archived webform displays archive message.
+    $this->drupalGet('/form/test-form-archived');
+    $this->assertRaw('This webform is <a href="' . $base_path . 'admin/structure/webform/manage/test_form_archived/settings">archived</a>');
+
+    // Check that archived webform is remove webform select menu.
+    $this->drupalGet('/node/add/webform');
+    $this->assertRaw('<option value="contact">Contact</option>');
+    $this->assertNoRaw('Test: Webform: Archive');
+
+    // Check that selected archived webform is preserved in webform select menu.
+    $this->drupalGet('/node/add/webform', ['query' => ['webform_id' => 'test_form_archived']]);
+    $this->assertRaw('<option value="contact">Contact</option>');
+    $this->assertRaw('<optgroup label="Archived"><option value="test_form_archived" selected="selected">Test: Webform: Archive</option></optgroup>');
+
+    // Change the archived webform to be a template.
+    $webform->set('template', TRUE);
+    $webform->save();
+
+    // Change archived webform to template.
+    $this->drupalGet('/admin/structure/webform');
+    $this->assertRaw('Contact');
+    $this->assertNoRaw('Test: Webform: Archive');
+
+    // Check that archived template with (Template) label appears when archived filter selected.
+    $this->drupalGet('/admin/structure/webform', ['query' => ['state' => 'archived']]);
+    $this->assertNoRaw('Contact');
+    $this->assertRaw('<td><a href="' . $base_path . 'form/test-form-archived">Test: Webform: Archive</a> <b>(Template)</b></td>');
+
+    // Check that archived template displays archive message
+    // (not template message).
+    $this->drupalGet('/form/test-form-archived');
+    $this->assertRaw('This webform is <a href="' . $base_path . 'admin/structure/webform/manage/test_form_archived/settings">archived</a>');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsAssetsTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsAssetsTest.php
index ab8afe0bdefbb41e41cd96da1e68c8955ae70ea7..3205c538bc4220f8de8e26051658542caa03bbe0 100644
--- a/web/modules/webform/src/Tests/Settings/WebformSettingsAssetsTest.php
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsAssetsTest.php
@@ -26,7 +26,7 @@ public function testAssets() {
     $webform_assets = Webform::load('test_form_assets');
 
     // Check has CSS and JavaScript.
-    $this->drupalGet('webform/test_form_assets');
+    $this->drupalGet('/webform/test_form_assets');
     $this->assertRaw('<link rel="stylesheet" href="' . base_path() . 'webform/css/test_form_assets?');
     $this->assertRaw('<script src="' . base_path() . 'webform/javascript/test_form_assets?');
 
@@ -34,7 +34,7 @@ public function testAssets() {
     $webform_assets->setCss('')->setJavaScript('')->save();
 
     // Check has no CSS or JavaScript.
-    $this->drupalGet('webform/test_form_assets');
+    $this->drupalGet('/webform/test_form_assets');
     $this->assertNoRaw('<link rel="stylesheet" href="' . base_path() . 'webform/css/test_form_assets?');
     $this->assertNoRaw('<script src="' . base_path() . 'webform/javascript/test_form_assets?');
 
@@ -45,7 +45,7 @@ public function testAssets() {
       ->save();
 
     // Check has global CSS and JavaScript.
-    $this->drupalGet('webform/test_form_assets');
+    $this->drupalGet('/webform/test_form_assets');
     $this->assertRaw('<link rel="stylesheet" href="' . base_path() . 'webform/css/test_form_assets?');
     $this->assertRaw('<script src="' . base_path() . 'webform/javascript/test_form_assets?');
   }
diff --git a/web/modules/webform/src/Tests/WebformSubmissionFormAutofillTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsAutofillTest.php
similarity index 83%
rename from web/modules/webform/src/Tests/WebformSubmissionFormAutofillTest.php
rename to web/modules/webform/src/Tests/Settings/WebformSettingsAutofillTest.php
index 0e4e430b43e023a5210f128477bfc7d5528223ea..fe2a483cdb8b779e64e5389b0d8f89ad13c7aa2e 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionFormAutofillTest.php
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsAutofillTest.php
@@ -1,15 +1,16 @@
 <?php
 
-namespace Drupal\webform\Tests;
+namespace Drupal\webform\Tests\Settings;
 
 use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
 
 /**
  * Tests for webform submission form autofill.
  *
  * @group Webform
  */
-class WebformSubmissionFormAutofillTest extends WebformTestBase {
+class WebformSettingsAutofillTest extends WebformTestBase {
 
   /**
    * Webforms to load.
@@ -27,7 +28,7 @@ public function testAutofill() {
     $webform = Webform::load('test_form_autofill');
 
     // Check that elements are both blank.
-    $this->drupalGet('webform/test_form_autofill');
+    $this->drupalGet('/webform/test_form_autofill');
     $this->assertNoRaw('This submission has been autofilled with your previous submission.');
     $this->assertFieldByName('textfield_autofill', '');
     $this->assertFieldByName('textfield_excluded', '');
@@ -41,13 +42,13 @@ public function testAutofill() {
 
     // Check that 'textfield_autofill' is autofilled and 'textfield_excluded'
     // is empty.
-    $this->drupalGet('webform/test_form_autofill');
+    $this->drupalGet('/webform/test_form_autofill');
     $this->assertFieldByName('textfield_autofill', '{textfield_autofill}');
     $this->assertNoFieldByName('textfield_autofill', '{textfield_excluded}');
     $this->assertFieldByName('textfield_excluded', '');
 
     // Check that default configuration message is displayed.
-    $this->drupalGet('webform/test_form_autofill');
+    $this->drupalGet('/webform/test_form_autofill');
     $this->assertFieldByName('textfield_autofill', '{textfield_autofill}');
     $this->assertRaw('This submission has been autofilled with your previous submission.');
 
@@ -57,7 +58,7 @@ public function testAutofill() {
       ->save();
 
     // Check no autofill message is displayed.
-    $this->drupalGet('webform/test_form_autofill');
+    $this->drupalGet('/webform/test_form_autofill');
     $this->assertFieldByName('textfield_autofill', '{textfield_autofill}');
     $this->assertNoRaw('This submission has been autofilled with your previous submission.');
 
@@ -67,7 +68,7 @@ public function testAutofill() {
       ->save();
 
     // Check custom autofill message is displayed.
-    $this->drupalGet('webform/test_form_autofill');
+    $this->drupalGet('/webform/test_form_autofill');
     $this->assertFieldByName('textfield_autofill', '{textfield_autofill}');
     $this->assertRaw('{autofill_message}');
   }
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsBehaviorsTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsBehaviorsTest.php
index b5345ece84bcc08eafea7945f128a9537d6bdc16..219c5ba1919e8ffdf6a321bc07e2ad45d271bd0b 100644
--- a/web/modules/webform/src/Tests/Settings/WebformSettingsBehaviorsTest.php
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsBehaviorsTest.php
@@ -52,7 +52,7 @@ public function testSettings() {
     $webform_form_submit_once = Webform::load('test_form_submit_once');
 
     // Check webform has webform.form.submit_once.js.
-    $this->drupalGet('webform/test_form_submit_once');
+    $this->drupalGet('/webform/test_form_submit_once');
     $this->assertRaw('webform.form.submit_once.js');
 
     // Disable YAML specific form_submit_once setting.
@@ -60,11 +60,11 @@ public function testSettings() {
     $webform_form_submit_once->save();
 
     // Check submit once checkbox is enabled.
-    $this->drupalGet('admin/structure/webform/manage/test_form_submit_once/settings/form');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_submit_once/settings/form');
     $this->assertRaw('<input data-drupal-selector="edit-form-submit-once" aria-describedby="edit-form-submit-once--description" type="checkbox" id="edit-form-submit-once" name="form_submit_once" value class="form-checkbox" />');
 
     // Check webform no longer has webform.form.submit_once.js.
-    $this->drupalGet('webform/test_form_submit_once');
+    $this->drupalGet('/webform/test_form_submit_once');
     $this->assertNoRaw('webform.form.submit_once.js');
 
     // Enable default (global) submit_once on all webforms.
@@ -73,12 +73,12 @@ public function testSettings() {
       ->save();
 
     // Check submit_once checkbox is disabled.
-    $this->drupalGet('admin/structure/webform/manage/test_form_submit_once/settings/form');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_submit_once/settings/form');
     $this->assertRaw('<input data-drupal-selector="edit-form-submit-once-disabled" aria-describedby="edit-form-submit-once-disabled--description" disabled="disabled" type="checkbox" id="edit-form-submit-once-disabled" name="form_submit_once_disabled" value="1" checked="checked" class="form-checkbox" />');
     $this->assertRaw('Submit button is disabled immediately after it is clicked for all forms.');
 
     // Check webform has webform.form.submit_once.js.
-    $this->drupalGet('webform/test_form_submit_once');
+    $this->drupalGet('/webform/test_form_submit_once');
     $this->assertRaw('webform.form.submit_once.js');
 
     /**************************************************************************/
@@ -88,7 +88,7 @@ public function testSettings() {
     $webform_form_disable_back = Webform::load('test_form_disable_back');
 
     // Check webform has webform.form.disable_back.js.
-    $this->drupalGet('webform/test_form_disable_back');
+    $this->drupalGet('/webform/test_form_disable_back');
     $this->assertRaw('webform.form.disable_back.js');
 
     // Disable YAML specific form_disable_back setting.
@@ -96,11 +96,11 @@ public function testSettings() {
     $webform_form_disable_back->save();
 
     // Check disable_back checkbox is enabled.
-    $this->drupalGet('admin/structure/webform/manage/test_form_disable_back/settings/form');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_disable_back/settings/form');
     $this->assertRaw('<input data-drupal-selector="edit-form-disable-back" aria-describedby="edit-form-disable-back--description" type="checkbox" id="edit-form-disable-back" name="form_disable_back" value class="form-checkbox" />');
 
     // Check webform no longer has webform.form.disable_back.js.
-    $this->drupalGet('webform/test_form_disable_back');
+    $this->drupalGet('/webform/test_form_disable_back');
     $this->assertNoRaw('webform.form.disable_back.js');
 
     // Enable default (global) disable_back on all webforms.
@@ -109,12 +109,12 @@ public function testSettings() {
       ->save();
 
     // Check disable_back checkbox is disabled.
-    $this->drupalGet('admin/structure/webform/manage/test_form_disable_back/settings/form');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_disable_back/settings/form');
     $this->assertRaw('<input data-drupal-selector="edit-form-disable-back-disabled" aria-describedby="edit-form-disable-back-disabled--description" disabled="disabled" type="checkbox" id="edit-form-disable-back-disabled" name="form_disable_back_disabled" value="1" checked="checked" class="form-checkbox" />');
     $this->assertRaw('Back button is disabled for all forms.');
 
     // Check webform has webform.form.disable_back.js.
-    $this->drupalGet('webform/test_form_disable_back');
+    $this->drupalGet('/webform/test_form_disable_back');
     $this->assertRaw('webform.form.disable_back.js');
 
     /**************************************************************************/
@@ -124,7 +124,7 @@ public function testSettings() {
     $webform_form_submit_back = Webform::load('test_form_submit_back');
 
     // Check webform has webform.form.submit_back.js.
-    $this->drupalGet('webform/test_form_submit_back');
+    $this->drupalGet('/webform/test_form_submit_back');
     $this->assertRaw('webform.form.submit_back.js');
 
     // Disable YAML specific form_submit_back setting.
@@ -132,11 +132,11 @@ public function testSettings() {
     $webform_form_submit_back->save();
 
     // Check submit_back checkbox is enabled.
-    $this->drupalGet('admin/structure/webform/manage/test_form_submit_back/settings/form');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_submit_back/settings/form');
     $this->assertRaw('<input data-drupal-selector="edit-form-submit-back" aria-describedby="edit-form-submit-back--description" type="checkbox" id="edit-form-submit-back" name="form_submit_back" value class="form-checkbox" />');
 
     // Check webform no longer has webform.form.submit_back.js.
-    $this->drupalGet('webform/test_form_submit_back');
+    $this->drupalGet('/webform/test_form_submit_back');
     $this->assertNoRaw('webform.form.submit_back.js');
 
     // Enable default (global) submit_back on all webforms.
@@ -145,12 +145,12 @@ public function testSettings() {
       ->save();
 
     // Check submit_back checkbox is disabled.
-    $this->drupalGet('admin/structure/webform/manage/test_form_submit_back/settings/form');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_submit_back/settings/form');
     $this->assertRaw('<input data-drupal-selector="edit-form-submit-back-disabled" aria-describedby="edit-form-submit-back-disabled--description" disabled="disabled" type="checkbox" id="edit-form-submit-back-disabled" name="form_submit_back_disabled" value="1" checked="checked" class="form-checkbox" />');
     $this->assertRaw('Browser back button submits the previous page for all forms.');
 
     // Check webform has webform.form.submit_back.js.
-    $this->drupalGet('webform/test_form_submit_back');
+    $this->drupalGet('/webform/test_form_submit_back');
     $this->assertRaw('webform.form.submit_back.js');
 
     /**************************************************************************/
@@ -160,7 +160,7 @@ public function testSettings() {
     $webform_form_unsaved = Webform::load('test_form_unsaved');
 
     // Check webform has .js-webform-unsaved class.
-    $this->drupalGet('webform/test_form_unsaved');
+    $this->drupalGet('/webform/test_form_unsaved');
     $this->assertCssSelect('form.js-webform-unsaved', t('Form has .js-webform-unsaved class.'));
 
     // Disable YAML specific webform unsaved setting.
@@ -168,11 +168,11 @@ public function testSettings() {
     $webform_form_unsaved->save();
 
     // Check novalidate checkbox is enabled.
-    $this->drupalGet('admin/structure/webform/manage/test_form_unsaved/settings/form');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_unsaved/settings/form');
     $this->assertRaw('<input data-drupal-selector="edit-form-unsaved" aria-describedby="edit-form-unsaved--description" type="checkbox" id="edit-form-unsaved" name="form_unsaved" value class="form-checkbox" />');
 
     // Check webform no longer has .js-webform-unsaved class.
-    $this->drupalGet('webform/test_form_novalidate');
+    $this->drupalGet('/webform/test_form_novalidate');
     $this->assertNoCssSelect('webform.js-webform-unsaved', t('Webform does not have .js-webform-unsaved class.'));
 
     // Enable default (global) unsaved on all webforms.
@@ -181,12 +181,12 @@ public function testSettings() {
       ->save();
 
     // Check unsaved checkbox is disabled.
-    $this->drupalGet('admin/structure/webform/manage/test_form_unsaved/settings/form');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_unsaved/settings/form');
     $this->assertRaw('<input data-drupal-selector="edit-form-unsaved-disabled" aria-describedby="edit-form-unsaved-disabled--description" disabled="disabled" type="checkbox" id="edit-form-unsaved-disabled" name="form_unsaved_disabled" value="1" checked="checked" class="form-checkbox" />');
     $this->assertRaw('Unsaved warning is enabled for all forms.');
 
     // Check unsaved attribute added to webform.
-    $this->drupalGet('webform/test_form_unsaved');
+    $this->drupalGet('/webform/test_form_unsaved');
     $this->assertCssSelect('form.js-webform-unsaved', t('Form has .js-webform-unsaved class.'));
 
     /**************************************************************************/
@@ -194,7 +194,7 @@ public function testSettings() {
     /**************************************************************************/
 
     // Check webform has autocomplete=off attribute.
-    $this->drupalGet('webform/test_form_disable_autocomplete');
+    $this->drupalGet('/webform/test_form_disable_autocomplete');
     $this->assertCssSelect('form[autocomplete="off"]', t('Form has autocomplete=off attribute.'));
 
     /**************************************************************************/
@@ -204,7 +204,7 @@ public function testSettings() {
     $webform_form_novalidate = Webform::load('test_form_novalidate');
 
     // Check webform has novalidate attribute.
-    $this->drupalGet('webform/test_form_novalidate');
+    $this->drupalGet('/webform/test_form_novalidate');
     $this->assertCssSelect('form[novalidate="novalidate"]', t('Form has the proper novalidate attribute.'));
 
     // Disable YAML specific webform client-side validation setting.
@@ -212,12 +212,12 @@ public function testSettings() {
     $webform_form_novalidate->save();
 
     // Check novalidate checkbox is enabled.
-    $this->drupalGet('admin/structure/webform/manage/test_form_novalidate/settings/form');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_novalidate/settings/form');
     $this->assertRaw('<input data-drupal-selector="edit-form-novalidate" aria-describedby="edit-form-novalidate--description" type="checkbox" id="edit-form-novalidate" name="form_novalidate" value class="form-checkbox" />');
     $this->assertRaw('If checked, the <a href="http://www.w3schools.com/tags/att_form_novalidate.asp">novalidate</a> attribute, which disables client-side validation, will be added to this form.');
 
     // Check webform no longer has novalidate attribute.
-    $this->drupalGet('webform/test_form_novalidate');
+    $this->drupalGet('/webform/test_form_novalidate');
     $this->assertNoCssSelect('form[novalidate="novalidate"]', t('Webform have client-side validation enabled.'));
 
     // Enable default (global) disable client-side validation on all webforms.
@@ -226,13 +226,13 @@ public function testSettings() {
       ->save();
 
     // Check novalidate checkbox is disabled.
-    $this->drupalGet('admin/structure/webform/manage/test_form_novalidate/settings/form');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_novalidate/settings/form');
     $this->assertNoRaw('If checked, the <a href="http://www.w3schools.com/tags/att_form_novalidate.asp">novalidate</a> attribute, which disables client-side validation, will be added to this form.');
     $this->assertRaw('<input data-drupal-selector="edit-form-novalidate-disabled" aria-describedby="edit-form-novalidate-disabled--description" disabled="disabled" type="checkbox" id="edit-form-novalidate-disabled" name="form_novalidate_disabled" value="1" checked="checked" class="form-checkbox" />');
     $this->assertRaw('Client-side validation is disabled for all forms.');
 
     // Check novalidate attribute added to webform.
-    $this->drupalGet('webform/test_form_novalidate');
+    $this->drupalGet('/webform/test_form_novalidate');
     $this->assertCssSelect('form[novalidate="novalidate"]', t('Form has the proper novalidate attribute.'));
 
     /**************************************************************************/
@@ -242,7 +242,7 @@ public function testSettings() {
     $webform_form_required = Webform::load('test_form_required');
 
     // Check webform has required indicator.
-    $this->drupalGet('webform/test_form_required');
+    $this->drupalGet('/webform/test_form_required');
     $this->assertRaw('Indicates required field');
 
     // Disable required indicator.
@@ -250,7 +250,7 @@ public function testSettings() {
     $webform_form_required->save();
 
     // Check webform does not have have required indicator.
-    $this->drupalGet('webform/test_form_required');
+    $this->drupalGet('/webform/test_form_required');
     $this->assertNoRaw('Indicates required field');
 
     // Enable default (global) required indicator on all webforms.
@@ -260,12 +260,12 @@ public function testSettings() {
       ->save();
 
     // Check required checkbox is disabled.
-    $this->drupalGet('admin/structure/webform/manage/test_form_required/settings/form');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_required/settings/form');
     $this->assertRaw('Required indicator is displayed on all forms.');
     $this->assertRaw('<input data-drupal-selector="edit-form-required-disabled" aria-describedby="edit-form-required-disabled--description" disabled="disabled" type="checkbox" id="edit-form-required-disabled" name="form_required_disabled" value="1" checked="checked" class="form-checkbox" />');
 
     // Check global required indicator added to webform.
-    $this->drupalGet('webform/test_form_required');
+    $this->drupalGet('/webform/test_form_required');
     $this->assertRaw('Custom required field');
 
     $elements = $webform_form_required->getElementsDecoded();
@@ -274,7 +274,7 @@ public function testSettings() {
     $webform_form_required->save();
 
     // Check required indicator not added to webform with no required elements.
-    $this->drupalGet('webform/test_form_required');
+    $this->drupalGet('/webform/test_form_required');
     $this->assertNoRaw('Custom required field');
 
     /**************************************************************************/
@@ -282,7 +282,7 @@ public function testSettings() {
     /**************************************************************************/
 
     // Check webform has autofocus class.
-    $this->drupalGet('webform/test_form_autofocus');
+    $this->drupalGet('/webform/test_form_autofocus');
     $this->assertCssSelect('.js-webform-autofocus');
 
     /**************************************************************************/
@@ -292,11 +292,11 @@ public function testSettings() {
     $webform_form_details_toggle = Webform::load('test_form_details_toggle');
 
     // Check webform has .webform-details-toggle class.
-    $this->drupalGet('webform/test_form_details_toggle');
+    $this->drupalGet('/webform/test_form_details_toggle');
     $this->assertCssSelect('form.webform-details-toggle', t('Form has the .webform-details-toggle class.'));
 
     // Check details toggle checkbox is disabled.
-    $this->drupalGet('admin/structure/webform/manage/test_form_details_toggle/settings/form');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_details_toggle/settings/form');
     $this->assertRaw('<input data-drupal-selector="edit-form-details-toggle-disabled" aria-describedby="edit-form-details-toggle-disabled--description" disabled="disabled" type="checkbox" id="edit-form-details-toggle-disabled" name="form_details_toggle_disabled" value="1" checked="checked" class="form-checkbox" />');
     $this->assertRaw('Expand/collapse all (details) link is automatically added to all forms.');
 
@@ -306,11 +306,11 @@ public function testSettings() {
       ->save();
 
     // Check .webform-details-toggle class still added to webform.
-    $this->drupalGet('webform/test_form_details_toggle');
+    $this->drupalGet('/webform/test_form_details_toggle');
     $this->assertCssSelect('form.webform-details-toggle', t('Form has the .webform-details-toggle class.'));
 
     // Check details toggle checkbox is enabled.
-    $this->drupalGet('admin/structure/webform/manage/test_form_details_toggle/settings/form');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_details_toggle/settings/form');
     $this->assertRaw('<input data-drupal-selector="edit-form-details-toggle" aria-describedby="edit-form-details-toggle--description" type="checkbox" id="edit-form-details-toggle" name="form_details_toggle" value checked="checked" class="form-checkbox" />');
     $this->assertRaw('If checked, an expand/collapse all (details) link will be added to this webform when there are two or more details elements available on the webform.');
 
@@ -319,73 +319,56 @@ public function testSettings() {
     $webform_form_details_toggle->save();
 
     // Check webform does not hav .webform-details-toggle class.
-    $this->drupalGet('webform/test_form_details_toggle');
+    $this->drupalGet('/webform/test_form_details_toggle');
     $this->assertNoCssSelect('webform.webform-details-toggle', t('Webform does not have the .webform-details-toggle class.'));
 
     /**************************************************************************/
-    /* Test webform disable inline form errors  (test_form_disable_inline_errors) */
+    /* Test webform disable inline form errors (test_form_disable_inline_errors) */
     /**************************************************************************/
 
     $webform_form_inline_errors = Webform::load('test_form_disable_inline_errors');
-    if (floatval(\Drupal::VERSION) >= 8.5) {
-      // Check that error message is displayed at the top of the page.
-      $this->postSubmission($webform_form_inline_errors);
-      $this->assertPattern('#<h2 class="visually-hidden">Error message</h2>\s+textfield field is required.#m');
-
-      // Enable the inline form errors module.
-      \Drupal::service('module_installer')->install(['inline_form_errors']);
-
-      // Check that error message is still displayed at the top of the page.
-      $this->postSubmission($webform_form_inline_errors);
-      $this->assertPattern('#<h2 class="visually-hidden">Error message</h2>\s+textfield field is required.#m');
-
-      // Allow inline error message for this form.
-      $webform_form_inline_errors->setSetting('form_disable_inline_errors', FALSE);
-      $webform_form_inline_errors->save();
-
-      // Check that error message is not displayed at the top of the page.
-      $this->postSubmission($webform_form_inline_errors);
-      $this->assertNoPattern('#<h2 class="visually-hidden">Error message</h2>\s+textfield field is required.#m');
-
-      // Check that error message is displayed inline.
-      $this->assertRaw('1 error has been found: <div class="item-list--comma-list item-list"><ul class="item-list__comma-list"><li><a href="#edit-textfield">textfield</a></li></ul>');
-      $this->assertRaw('<strong>textfield field is required.</strong>');
-
-      // Check disable inline errors checkbox is enabled.
-      $this->drupalGet('admin/structure/webform/manage/test_form_disable_inline_errors/settings/form');
-      $this->assertRaw('<input data-drupal-selector="edit-form-disable-inline-errors" aria-describedby="edit-form-disable-inline-errors--description" type="checkbox" id="edit-form-disable-inline-errors" name="form_disable_inline_errors" value class="form-checkbox" />');
-      $this->assertRaw('If checked, <a href="https://www.drupal.org/docs/8/core/modules/inline-form-errors/inline-form-errors-module-overview">inline form errors</a> will be disabled for this form.');
-
-      // Enable default (global) disable inline form errors on all webforms.
-      \Drupal::configFactory()->getEditable('webform.settings')
-        ->set('settings.default_form_disable_inline_errors', TRUE)
-        ->save();
-
-      // Check novalidate checkbox is disabled.
-      $this->drupalGet('admin/structure/webform/manage/test_form_disable_inline_errors/settings/form');
-      $this->assertRaw('<input data-drupal-selector="edit-form-disable-inline-errors-disabled" aria-describedby="edit-form-disable-inline-errors-disabled--description" disabled="disabled" type="checkbox" id="edit-form-disable-inline-errors-disabled" name="form_disable_inline_errors_disabled" value="1" checked="checked" class="form-checkbox" />');
-      $this->assertRaw('Inline form errors is disabled for all forms.');
-
-      // Check that error message is not displayed inline.
-      $this->assertNoRaw('1 error has been found: <div class="item-list--comma-list item-list"><ul class="item-list__comma-list"><li><a href="#edit-textfield">textfield</a></li></ul>');
-      $this->assertNoRaw('<strong>textfield field is required.</strong>');
-    }
-    else {
-      // Check that error message is displayed at the top of the page.
-      $this->postSubmission($webform_form_inline_errors);
-      $this->assertPattern('#<h2 class="visually-hidden">Error message</h2>\s+textfield field is required.#m');
-
-      // Enable the inline form errors module.
-      \Drupal::service('module_installer')->install(['inline_form_errors']);
-
-      // Check that error message is NOT displayed at the top of the page.
-      $this->postSubmission($webform_form_inline_errors);
-      $this->assertNoPattern('#<h2 class="visually-hidden">Error message</h2>\s+textfield field is required.#m');
-
-      // Check that disable inline form errors is not visible in < 8.4.x.
-      $this->drupalGet('admin/structure/webform/config');
-      $this->assertNoRaw('Disable inline form errors for all webforms');
-    }
+
+    // Check that error message is displayed at the top of the page.
+    $this->postSubmission($webform_form_inline_errors);
+    $this->assertPattern('#<h2 class="visually-hidden">Error message</h2>\s+textfield field is required.#m');
+
+    // Enable the inline form errors module.
+    \Drupal::service('module_installer')->install(['inline_form_errors']);
+
+    // Check that error message is still displayed at the top of the page.
+    $this->postSubmission($webform_form_inline_errors);
+    $this->assertPattern('#<h2 class="visually-hidden">Error message</h2>\s+textfield field is required.#m');
+
+    // Allow inline error message for this form.
+    $webform_form_inline_errors->setSetting('form_disable_inline_errors', FALSE);
+    $webform_form_inline_errors->save();
+
+    // Check that error message is not displayed at the top of the page.
+    $this->postSubmission($webform_form_inline_errors);
+    $this->assertNoPattern('#<h2 class="visually-hidden">Error message</h2>\s+textfield field is required.#m');
+
+    // Check that error message is displayed inline.
+    $this->assertRaw('1 error has been found: <div class="item-list--comma-list item-list"><ul class="item-list__comma-list"><li><a href="#edit-textfield">textfield</a></li></ul>');
+    $this->assertRaw('<strong>textfield field is required.</strong>');
+
+    // Check disable inline errors checkbox is enabled.
+    $this->drupalGet('/admin/structure/webform/manage/test_form_disable_inline_errors/settings/form');
+    $this->assertRaw('<input data-drupal-selector="edit-form-disable-inline-errors" aria-describedby="edit-form-disable-inline-errors--description" type="checkbox" id="edit-form-disable-inline-errors" name="form_disable_inline_errors" value class="form-checkbox" />');
+    $this->assertRaw('If checked, <a href="https://www.drupal.org/docs/8/core/modules/inline-form-errors/inline-form-errors-module-overview">inline form errors</a> will be disabled for this form.');
+
+    // Enable default (global) disable inline form errors on all webforms.
+    \Drupal::configFactory()->getEditable('webform.settings')
+      ->set('settings.default_form_disable_inline_errors', TRUE)
+      ->save();
+
+    // Check novalidate checkbox is disabled.
+    $this->drupalGet('/admin/structure/webform/manage/test_form_disable_inline_errors/settings/form');
+    $this->assertRaw('<input data-drupal-selector="edit-form-disable-inline-errors-disabled" aria-describedby="edit-form-disable-inline-errors-disabled--description" disabled="disabled" type="checkbox" id="edit-form-disable-inline-errors-disabled" name="form_disable_inline_errors_disabled" value="1" checked="checked" class="form-checkbox" />');
+    $this->assertRaw('Inline form errors is disabled for all forms.');
+
+    // Check that error message is not displayed inline.
+    $this->assertNoRaw('1 error has been found: <div class="item-list--comma-list item-list"><ul class="item-list__comma-list"><li><a href="#edit-textfield">textfield</a></li></ul>');
+    $this->assertNoRaw('<strong>textfield field is required.</strong>');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsConfidentialTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsConfidentialTest.php
index fa821cd7417eb3d7594ec3bb0981f1ddad0e9921..5cd560d8e1e3c49d5395069654255de11bc604d1 100644
--- a/web/modules/webform/src/Tests/Settings/WebformSettingsConfidentialTest.php
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsConfidentialTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\webform\Tests\Settings;
 
+use Drupal\user\Entity\Role;
 use Drupal\webform\Entity\Webform;
 use Drupal\webform\Entity\WebformSubmission;
 use Drupal\webform\Tests\WebformTestBase;
@@ -20,56 +21,56 @@ class WebformSettingsConfidentialTest extends WebformTestBase {
    */
   protected static $testWebforms = ['test_form_confidential'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    $this->addWebformSubmissionOwnPermissionsToAnonymous();
-  }
-
   /**
    * Tests webform confidential setting.
    */
   public function testConfidential() {
+    /** @var \Drupal\user\RoleInterface $anonymous_role */
+    $anonymous_role = Role::load('anonymous');
+    $anonymous_role->grantPermission('view own webform submission')
+      ->grantPermission('edit own webform submission')
+      ->grantPermission('delete own webform submission')
+      ->save();
+
+    /**************************************************************************/
+
     $this->drupalLogin($this->rootUser);
 
-    $webform_confidential = Webform::load('test_form_confidential');
+    $webform = Webform::load('test_form_confidential');
 
     // Check logout warning when accessing webform.
-    $this->drupalGet('webform/test_form_confidential');
+    $this->drupalGet('/webform/test_form_confidential');
     $this->assertNoFieldById('edit-name');
     $this->assertRaw('This form is confidential.');
 
     // Check no logout warning when testing webform.
-    $this->drupalGet('webform/test_form_confidential/test');
+    $this->drupalGet('/webform/test_form_confidential/test');
     $this->assertFieldById('edit-name');
     $this->assertNoRaw('This form is confidential.');
 
-    // Check that test submission does not record the IP address
-    $sid = $this->postSubmissionTest($webform_confidential, ['name' => 'John']);
+    // Check that test submission does not record the IP address.
+    $sid = $this->postSubmissionTest($webform, ['name' => 'John']);
     $webform_submission = WebformSubmission::load($sid);
     $this->assertEqual($webform_submission->getRemoteAddr(), t('(unknown)'));
     $this->assertEqual($webform_submission->getOwnerId(), 0);
 
     // Check anonymous access to webform.
     $this->drupalLogout();
-    $this->drupalGet('webform/test_form_confidential');
+    $this->drupalGet('/webform/test_form_confidential');
     $this->assertFieldById('edit-name');
     $this->assertNoRaw('This form is confidential.');
 
     // Check that submission does not track the requests IP address.
-    $sid = $this->postSubmission($webform_confidential, ['name' => 'John']);
+    $sid = $this->postSubmission($webform, ['name' => 'John']);
     $webform_submission = WebformSubmission::load($sid);
     $this->assertEqual($webform_submission->getRemoteAddr(), t('(unknown)'));
     $this->assertEqual($webform_submission->getOwnerId(), 0);
 
     // Check that previous submissions are visible.
-    $this->drupalGet('webform/test_form_confidential');
+    $this->drupalGet('/webform/test_form_confidential');
     $this->assertRaw('View your previous submission');
 
-    // Check that anonymous submissison is not converted to authenticated.
+    // Check that anonymous submission is not converted to authenticated.
     // @see \Drupal\webform\WebformSubmissionStorage::userLogin
     $this->drupalLogin($this->rootUser);
     $webform_submission = $this->loadSubmission($sid);
@@ -77,7 +78,7 @@ public function testConfidential() {
 
     // Check that previous submissions $_SESSION was unset after login/logout.
     $this->drupalLogout();
-    $this->drupalGet('webform/test_form_confidential');
+    $this->drupalGet('/webform/test_form_confidential');
     $this->assertNoRaw('View your previous submission.');
   }
 
diff --git a/web/modules/webform/src/Tests/WebformSubmissionFormConfirmationTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsConfirmationTest.php
similarity index 83%
rename from web/modules/webform/src/Tests/WebformSubmissionFormConfirmationTest.php
rename to web/modules/webform/src/Tests/Settings/WebformSettingsConfirmationTest.php
index 21f8c1bd35a4bc6b12996318d6ab0791488112f1..f03b5caefa8bed6fc05ab85ca2bf40235c37341f 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionFormConfirmationTest.php
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsConfirmationTest.php
@@ -1,23 +1,33 @@
 <?php
 
-namespace Drupal\webform\Tests;
+namespace Drupal\webform\Tests\Settings;
 
 use Drupal\webform\Entity\Webform;
 use Drupal\webform\Entity\WebformSubmission;
+use Drupal\webform\Tests\WebformTestBase;
 
 /**
  * Tests for webform submission form confirmation.
  *
  * @group Webform
  */
-class WebformSubmissionFormConfirmationTest extends WebformTestBase {
+class WebformSettingsConfirmationTest extends WebformTestBase {
 
   /**
    * Webforms to load.
    *
    * @var array
    */
-  protected static $testWebforms = ['test_confirmation_message', 'test_confirmation_modal', 'test_confirmation_inline', 'test_confirmation_page', 'test_confirmation_page_custom', 'test_confirmation_url', 'test_confirmation_url_message'];
+  protected static $testWebforms = [
+    'test_confirmation_message',
+    'test_confirmation_modal',
+    'test_confirmation_inline',
+    'test_confirmation_page',
+    'test_confirmation_page_custom',
+    'test_confirmation_url',
+    'test_confirmation_url_message',
+    'test_confirmation_none',
+  ];
 
   /**
    * {@inheritdoc}
@@ -69,12 +79,12 @@ public function testConfirmation() {
 
     // Check confirmation inline.
     $this->drupalPostForm('webform/test_confirmation_inline', [], t('Submit'));
-    $this->assertRaw('<a href="' . $webform_confirmation_inline->toUrl('canonical', ['absolute' => TRUE])->toString() . '" rel="back" title="Back to form">Back to form</a>');
+    $this->assertRaw('<a href="' . $webform_confirmation_inline->toUrl('canonical', ['absolute' => TRUE])->toString() . '" rel="prev" title="Back to form">Back to form</a>');
     $this->assertUrl('webform/test_confirmation_inline');
 
     // Check confirmation inline with custom query parameters.
     $this->drupalPostForm('webform/test_confirmation_inline', [], t('Submit'), ['query' => ['custom' => 'param']]);
-    $this->assertRaw('<a href="' . $webform_confirmation_inline->toUrl('canonical', ['absolute' => TRUE, 'query' => ['custom' => 'param']])->toString() . '" rel="back" title="Back to form">Back to form</a>');
+    $this->assertRaw('<a href="' . $webform_confirmation_inline->toUrl('canonical', ['absolute' => TRUE, 'query' => ['custom' => 'param']])->toString() . '" rel="prev" title="Back to form">Back to form</a>');
     $this->assertUrl('webform/test_confirmation_inline', ['query' => ['custom' => 'param']]);
 
     /* Test confirmation page (confirmation_type=page) */
@@ -85,12 +95,12 @@ public function testConfirmation() {
     $sid = $this->postSubmission($webform_confirmation_page);
     $webform_submission = WebformSubmission::load($sid);
     $this->assertRaw('This is a custom confirmation page.');
-    $this->assertRaw('<a href="' . $webform_confirmation_page->toUrl('canonical', ['absolute' => TRUE])->toString() . '" rel="back" title="Back to form">Back to form</a>');
+    $this->assertRaw('<a href="' . $webform_confirmation_page->toUrl('canonical', ['absolute' => TRUE])->toString() . '" rel="prev" title="Back to form">Back to form</a>');
     $this->assertUrl('webform/test_confirmation_page/confirmation', ['query' => ['token' => $webform_submission->getToken()]]);
 
     // Check that the confirmation page's 'Back to form 'link includes custom
     // query parameters.
-    $this->drupalGet('webform/test_confirmation_page/confirmation', ['query' => ['custom' => 'param']]);
+    $this->drupalGet('/webform/test_confirmation_page/confirmation', ['query' => ['custom' => 'param']]);
 
     // Check confirmation page with custom query parameters.
     $sid = $this->postSubmission($webform_confirmation_page, [], NULL, ['query' => ['custom' => 'param']]);
@@ -109,7 +119,7 @@ public function testConfirmation() {
     $this->postSubmission($webform_confirmation_page);
     $this->assertUrl('webform/test_confirmation_page/confirmation');
 
-    // TODO: (TESTING)  Figure out why the inline confirmation link is not including the query string parameters.
+    // TODO: (TESTING) Figure out why the inline confirmation link is not including the query string parameters.
     // $this->assertRaw('<a href="' . $webform_confirmation_page->toUrl()->toString() . '?custom=param">Back to form</a>');.
 
     /* Test confirmation page custom (confirmation_type=page) */
@@ -120,13 +130,13 @@ public function testConfirmation() {
     $this->postSubmission($webform_confirmation_page_custom);
     $this->assertRaw('<h1 class="page-title">Custom confirmation page title</h1>');
     $this->assertRaw('<div style="border: 10px solid red; padding: 1em;" class="webform-confirmation">');
-    $this->assertRaw('<a href="' . $webform_confirmation_page_custom->toUrl()->setAbsolute()->toString() . '" rel="back" title="Custom back to link" class="button">Custom back to link</a>');
+    $this->assertRaw('<a href="' . $webform_confirmation_page_custom->toUrl()->setAbsolute()->toString() . '" rel="prev" title="Custom back to link" class="button">Custom back to link</a>');
 
     // Check back link is hidden.
     $webform_confirmation_page_custom->setSetting('confirmation_back', FALSE);
     $webform_confirmation_page_custom->save();
     $this->postSubmission($webform_confirmation_page_custom);
-    $this->assertNoRaw('<a href="' . $webform_confirmation_page_custom->toUrl()->toString() . '" rel="back" title="Custom back to link" class="button">Custom back to link</a>');
+    $this->assertNoRaw('<a href="' . $webform_confirmation_page_custom->toUrl()->toString() . '" rel="prev" title="Custom back to link" class="button">Custom back to link</a>');
 
     /* Test confirmation URL (confirmation_type=url) */
 
@@ -146,6 +156,16 @@ public function testConfirmation() {
     $this->assertRaw('<h2 class="visually-hidden">Status message</h2>');
     $this->assertRaw('This is a custom confirmation message.');
     $this->assertUrl('<front>');
+
+    /* Test confirmation none (confirmation_type=none) */
+
+    $this->drupalLogout();
+    $webform_confirmation_url_message = Webform::load('test_confirmation_none');
+
+    // Check no confirmation message.
+    $this->postSubmission($webform_confirmation_url_message);
+    $this->assertNoRaw('<h2 class="visually-hidden">Status message</h2>');
+
   }
 
 }
diff --git a/web/modules/webform/src/Tests/WebformSubmissionFormDraftTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsDraftTest.php
similarity index 63%
rename from web/modules/webform/src/Tests/WebformSubmissionFormDraftTest.php
rename to web/modules/webform/src/Tests/Settings/WebformSettingsDraftTest.php
index 4f83c41206fece8ffb9dac1c2aaee586d41b0e3c..12799522fb30bd6ffa7edb020de5c4a6bd5ef596 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionFormDraftTest.php
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsDraftTest.php
@@ -1,16 +1,17 @@
 <?php
 
-namespace Drupal\webform\Tests;
+namespace Drupal\webform\Tests\Settings;
 
 use Drupal\webform\Entity\Webform;
 use Drupal\webform\Entity\WebformSubmission;
+use Drupal\webform\Tests\WebformTestBase;
 
 /**
  * Tests for webform submission form draft.
  *
  * @group Webform
  */
-class WebformSubmissionFormDraftTest extends WebformTestBase {
+class WebformSettingsDraftTest extends WebformTestBase {
 
   /**
    * Webforms to load.
@@ -19,20 +20,31 @@ class WebformSubmissionFormDraftTest extends WebformTestBase {
    */
   protected static $testWebforms = ['test_form_draft_authenticated', 'test_form_draft_anonymous', 'test_form_draft_multiple', 'test_form_preview'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
   /**
    * Test webform submission form draft.
    */
   public function testDraft() {
+    $normal_user = $this->drupalCreateUser(['view own webform submission']);
+
+    $admin_submission_user = $this->drupalCreateUser([
+      'administer webform submission',
+    ]);
+
+    /**************************************************************************/
+    // Draft access.
+    /**************************************************************************/
+
+    // Check access denied to review drafts when disabled.
+    $this->drupalGet('/webform/contact/drafts');
+    $this->assertResponse(403);
+
+    // Check access denied to review authenticated drafts.
+    $this->drupalGet('/webform/test_form_draft_authenticated/drafts');
+    $this->assertResponse(403);
+
+    // Check access allowed to review anonymous drafts.
+    $this->drupalGet('/webform/test_form_draft_anonymous/drafts');
+    $this->assertResponse(200);
 
     /**************************************************************************/
     // Autosave for anonymous draft to authenticated draft.
@@ -46,7 +58,7 @@ public function testDraft() {
       $is_authenticated = ($webform_id == 'test_form_draft_authenticated') ? TRUE : FALSE;
 
       // Login draft account.
-      ($is_authenticated) ? $this->drupalLogin($this->normalUser) : $this->drupalLogout();
+      ($is_authenticated) ? $this->drupalLogin($normal_user) : $this->drupalLogout();
 
       $webform = Webform::load($webform_id);
 
@@ -59,21 +71,33 @@ public function testDraft() {
       $this->assertRaw('Your draft has been saved');
       $this->assertNoRaw('You have an existing draft');
 
+      // Check access allowed to review drafts.
+      $this->drupalGet("webform/$webform_id/drafts");
+      $this->assertResponse(200);
+
       // Check loaded draft message.
       $this->drupalGet("webform/$webform_id");
       $this->assertNoRaw('Your draft has been saved');
       $this->assertRaw('You have an existing draft');
       $this->assertFieldByName('name', 'John Smith');
 
+      // Check no draft message when webform is closed.
+      $webform->setStatus(FALSE)->save();
+      $this->drupalGet("webform/$webform_id");
+      $this->assertNoRaw('You have an existing draft');
+      $this->assertNoFieldByName('name', 'John Smith');
+      $this->assertRaw('Sorry…This form is closed to new submissions.');
+      $webform->setStatus(TRUE)->save();
+
       // Login admin account.
-      $this->drupalLogin($this->adminSubmissionUser);
+      $this->drupalLogin($admin_submission_user);
 
       // Check submission.
       $this->drupalGet("admin/structure/webform/manage/$webform_id/submission/$sid");
       $this->assertRaw('<div><b>Is draft:</b> Yes</div>');
 
       // Login draft account.
-      ($is_authenticated) ? $this->drupalLogin($this->normalUser) : $this->drupalLogout();
+      ($is_authenticated) ? $this->drupalLogin($normal_user) : $this->drupalLogout();
 
       // Check update draft and bypass validation.
       $this->drupalPostForm("webform/$webform_id", [
@@ -126,24 +150,24 @@ public function testDraft() {
     $this->assertEqual($webform_submission->getOwnerId(), 0);
 
     // Check loaded draft message.
-    $this->drupalGet('webform/test_form_draft_anonymous');
+    $this->drupalGet('/webform/test_form_draft_anonymous');
     $this->assertRaw('You have an existing draft');
     $this->assertFieldByName('name', 'John Smith');
 
     // Login the normal user.
-    $this->drupalLogin($this->normalUser);
+    $this->drupalLogin($normal_user);
 
     // Check that submission is now owned by the normal user.
     \Drupal::entityTypeManager()->getStorage('webform_submission')->resetCache();
     $webform_submission = WebformSubmission::load($sid);
-    $this->assertEqual($webform_submission->getOwnerId(), $this->normalUser->id());
+    $this->assertEqual($webform_submission->getOwnerId(), $normal_user->id());
 
     // Check that drafts are not convert when form_convert_anonymous = FALSE.
     $this->drupalLogout();
     $webform->setSetting('form_convert_anonymous', FALSE)->save();
 
     $sid = $this->postSubmission($webform, ['name' => 'John Smith']);
-    $this->drupalLogin($this->normalUser);
+    $this->drupalLogin($normal_user);
 
     // Check that submission is still owned by anonymous user.
     \Drupal::entityTypeManager()->getStorage('webform_submission')->resetCache();
@@ -166,16 +190,16 @@ public function testDraft() {
     $this->assertEqual($webform_submission->getOwnerId(), 0);
 
     // Check loaded draft message does NOT appear on confidential submissions.
-    $this->drupalGet('webform/test_form_draft_anonymous');
+    $this->drupalGet('/webform/test_form_draft_anonymous');
     $this->assertRaw('You have an existing draft');
 
     // Login the normal user.
-    $this->drupalLogin($this->normalUser);
+    $this->drupalLogin($normal_user);
 
     \Drupal::entityTypeManager()->getStorage('webform_submission')->resetCache();
     $webform_submission = WebformSubmission::load($sid);
     // Check that submission is NOT owned by the normal user.
-    $this->assertNotEqual($webform_submission->getOwnerId(), $this->normalUser->id());
+    $this->assertNotEqual($webform_submission->getOwnerId(), $normal_user->id());
 
     // Check that submission is still anonymous.
     $this->assertEqual($webform_submission->getOwnerId(), 0);
@@ -184,35 +208,34 @@ public function testDraft() {
     // Export.
     /**************************************************************************/
 
-    $this->drupalLogin($this->adminSubmissionUser);
+    $this->drupalLogin($admin_submission_user);
 
     // Check export with draft settings.
-    $this->drupalGet('admin/structure/webform/manage/test_form_draft_authenticated/results/download');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_draft_authenticated/results/download');
     $this->assertFieldByName('state', 'all');
 
     // Check export without draft settings.
-    $this->drupalGet('admin/structure/webform/manage/test_form_preview/results/download');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_preview/results/download');
     $this->assertNoFieldByName('state', 'all');
 
     // Check autosave on submit with validation errors.
     $this->drupalPostForm('webform/test_form_draft_authenticated', [], t('Submit'));
     $this->assertRaw('Name field is required.');
-    $this->drupalGet('webform/test_form_draft_authenticated');
+    $this->drupalGet('/webform/test_form_draft_authenticated');
     $this->assertRaw('You have an existing draft');
 
     // Check autosave on preview.
     $this->drupalPostForm('webform/test_form_draft_authenticated', ['name' => 'John Smith'], t('Preview'));
     $this->assertRaw('Please review your submission.');
-    $this->drupalGet('webform/test_form_draft_authenticated');
+    $this->drupalGet('/webform/test_form_draft_authenticated');
     $this->assertRaw('You have an existing draft');
     $this->assertRaw('<label>Name</label>' . PHP_EOL . '        John Smith');
-  }
 
-  /**
-   * Test webform draft multiple.
-   */
-  public function testDraftMultiple() {
-    $this->drupalLogin($this->normalUser);
+    /**************************************************************************/
+    // Test webform draft multiple.
+    /**************************************************************************/
+
+    $this->drupalLogin($normal_user);
 
     $webform = Webform::load('test_form_draft_multiple');
 
@@ -222,38 +245,38 @@ public function testDraftMultiple() {
     $webform_submission_1 = WebformSubmission::load($sid_1);
 
     // Check restore first draft.
-    $this->drupalGet('webform/test_form_draft_multiple');
+    $this->drupalGet('/webform/test_form_draft_multiple');
     $this->assertNoRaw('You have saved drafts.');
     $this->assertRaw('You have a pending draft for this webform.');
     $this->assertFieldByName('name', '');
 
     // Check load pending draft using token.
-    $this->drupalGet('webform/test_form_draft_multiple');
+    $this->drupalGet('/webform/test_form_draft_multiple');
     $this->clickLink('Load your pending draft');
     $this->assertFieldByName('name', 'John Smith');
-    $this->drupalGet('webform/test_form_draft_multiple', ['query' => ['token' => $webform_submission_1->getToken()]]);
+    $this->drupalGet('/webform/test_form_draft_multiple', ['query' => ['token' => $webform_submission_1->getToken()]]);
     $this->assertFieldByName('name', 'John Smith');
 
     // Check user drafts.
-    $this->drupalGet('webform/test_form_draft_multiple/drafts');
+    $this->drupalGet('/webform/test_form_draft_multiple/drafts');
     $this->assertRaw('token=' . $webform_submission_1->getToken());
 
     // Save second draft.
     $sid_2 = $this->postSubmission($webform, ['name' => 'John Smith'], t('Save Draft'));
     $webform_submission_2 = WebformSubmission::load($sid_2);
     $this->assertRaw('Submission saved. You may return to this form later and it will restore the current values.');
-    $this->drupalGet('webform/test_form_draft_multiple');
+    $this->drupalGet('/webform/test_form_draft_multiple');
     $this->assertNoRaw('You have a pending draft for this webform.');
     $this->assertRaw('You have pending drafts for this webform. <a href="' . base_path() . 'webform/test_form_draft_multiple/drafts">View your pending drafts</a>.');
 
     // Check user drafts now has second draft.
-    $this->drupalGet('webform/test_form_draft_multiple/drafts');
+    $this->drupalGet('/webform/test_form_draft_multiple/drafts');
     $this->assertRaw('token=' . $webform_submission_1->getToken());
     $this->assertRaw('token=' . $webform_submission_2->getToken());
 
     // Check that anonymous user can't load drafts.
     $this->drupalLogout();
-    $this->drupalGet('webform/test_form_draft_multiple', ['query' => ['token' => $webform_submission_1->getToken()]]);
+    $this->drupalGet('/webform/test_form_draft_multiple', ['query' => ['token' => $webform_submission_1->getToken()]]);
     $this->assertFieldByName('name', '');
 
     // Save third anonymous draft.
@@ -261,14 +284,49 @@ public function testDraftMultiple() {
     $this->assertRaw('Submission saved. You may return to this form later and it will restore the current values.');
 
     // Check restore third anonymous draft.
-    $this->drupalGet('webform/test_form_draft_multiple');
+    $this->drupalGet('/webform/test_form_draft_multiple');
     $this->assertNoRaw('You have saved drafts.');
     $this->assertRaw('You have a pending draft for this webform.');
     $this->assertFieldByName('name', '');
 
-    $this->drupalGet('webform/test_form_draft_multiple');
+    $this->drupalGet('/webform/test_form_draft_multiple');
     $this->clickLink('Load your pending draft');
     $this->assertFieldByName('name', 'Jane Doe');
+
+    /**************************************************************************/
+    // Test webform submission form reset draft.
+    /**************************************************************************/
+
+    $this->drupalLogin($this->rootUser);
+
+    $webform = Webform::load('test_form_draft_authenticated');
+
+    // Check saved draft.
+    $sid = $this->postSubmission($webform, ['name' => 'John Smith'], t('Save Draft'));
+    $this->assertNotNull($sid);
+    $webform_submission = WebformSubmission::load($sid);
+    $this->assertEqual($sid, $webform_submission->id());
+
+    // Check reset delete's the draft.
+    $this->postSubmission($webform, [], t('Reset'));
+    \Drupal::entityTypeManager()->getStorage('webform_submission')->resetCache();
+    $webform_submission = WebformSubmission::load($sid);
+    $this->assertNull($webform_submission);
+
+    // Check submission with comment.
+    $sid = $this->postSubmission($webform, ['name' => 'John Smith', 'comment' => 'This is a comment'], t('Save Draft'));
+    $this->postSubmission($webform);
+    \Drupal::entityTypeManager()->getStorage('webform_submission')->resetCache();
+    $webform_submission = WebformSubmission::load($sid);
+    $this->assertEqual('This is a comment', $webform_submission->getElementData('comment'));
+
+    // Check submitted draft is not delete on reset.
+    $this->drupalPostForm('/admin/structure/webform/manage/test_form_draft_authenticated/submission/' . $sid . '/edit', ['comment' => 'This is ignored'], t('Reset'));
+    \Drupal::entityTypeManager()->getStorage('webform_submission')->resetCache();
+    $webform_submission = WebformSubmission::load($sid);
+    $this->assertEqual($sid, $webform_submission->id());
+    $this->assertEqual('This is a comment', $webform_submission->getElementData('comment'));
+    $this->assertNotEqual('This is ignored', $webform_submission->getElementData('comment'));
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsFormTitleTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsFormTitleTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..51c41e2bd2ff2ff0826f977bf946d23086d0705a
--- /dev/null
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsFormTitleTest.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\webform\Tests\Settings;
+
+use Drupal\Core\Serialization\Yaml;
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
+use Drupal\webform\WebformInterface;
+
+/**
+ * Tests for webform form title.
+ *
+ * @group Webform
+ */
+class WebformSettingsFormTitleTest extends WebformTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'node'];
+
+  /**
+   * Tests form title.
+   */
+  public function testTitle() {
+    $node = $this->drupalCreateNode(['title' => 'test_node']);
+
+    $webform = Webform::create([
+      'langcode' => 'en',
+      'status' => WebformInterface::STATUS_OPEN,
+      'id' => 'test_webform',
+      'title' => 'test_webform',
+      'elements' => Yaml::encode([
+        'test' => ['#markup' => 'test'],
+      ]),
+      'settings' => [
+        'form_prepopulate_source_entity' => TRUE,
+      ],
+    ]);
+    $webform->save();
+
+    $options = ['query' => ['source_entity_type' => 'node', 'source_entity_id' => $node->id()]];
+
+    /**************************************************************************/
+
+    // Check webform title.
+    $this->drupalGet('/webform/test_webform');
+    $this->assertRaw('<title>test_webform | Drupal</title>');
+
+    // Check (default) both title.
+    $this->drupalGet('/webform/test_webform', $options);
+    $this->assertRaw('<title>test_node: test_webform | Drupal</title>');
+
+    // Check webform and source entity title.
+    $webform
+      ->setSetting('form_title', WebformInterface::TITLE_WEBFORM_SOURCE_ENTITY)
+      ->save();
+    $this->drupalGet('/webform/test_webform', $options);
+    $this->assertRaw('<title>test_webform: test_node | Drupal</title>');
+
+    // Check source entity title.
+    $webform
+      ->setSetting('form_title', WebformInterface::TITLE_SOURCE_ENTITY)
+      ->save();
+    $this->drupalGet('/webform/test_webform', $options);
+    $this->assertRaw('<title>test_node | Drupal</title>');
+
+    // Check webform title.
+    $webform
+      ->setSetting('form_title', WebformInterface::TITLE_WEBFORM)
+      ->save();
+    $this->drupalGet('/webform/test_webform', $options);
+    $this->assertRaw('<title>test_webform | Drupal</title>');
+
+    // Check duplicate titles.
+    $webform
+      ->setSetting('form_title', WebformInterface::TITLE_SOURCE_ENTITY_WEBFORM)
+      ->save();
+    $this->drupalGet('/webform/test_webform', $options);
+    $this->assertRaw('<title>test_node: test_webform | Drupal</title>');
+    $webform->set('title', 'test_node')
+      ->save();
+    $this->drupalGet('/webform/test_webform', $options);
+    $this->assertRaw('<title>test_node | Drupal</title>');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsLimitUniqueTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsLimitUniqueTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..19bb3e94ab570f087b7685de05e74ca474b1226d
--- /dev/null
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsLimitUniqueTest.php
@@ -0,0 +1,164 @@
+<?php
+
+namespace Drupal\webform\Tests\Settings;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform_node\Tests\WebformNodeTestBase;
+
+/**
+ * Tests for webform submission form unique limit.
+ *
+ * @group Webform
+ */
+class WebformSettingsLimitUniqueTest extends WebformNodeTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'webform_node'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_form_limit_total_unique', 'test_form_limit_user_unique'];
+
+  /**
+   * Tests webform submission form unique limit.
+   */
+  public function testLimitUnique() {
+    $admin_user = $this->drupalCreateUser(['administer webform']);
+
+    $webform_total_unique = Webform::load('test_form_limit_total_unique');
+    $webform_user_unique = Webform::load('test_form_limit_user_unique');
+
+    /**************************************************************************/
+    // Total unique. (webform)
+    /**************************************************************************/
+
+    // Check that name is empty for new submission for admin user.
+    $this->drupalLogin($admin_user);
+    $this->drupalGet('/webform/test_form_limit_total_unique');
+    $this->assertFieldByName('name', '');
+
+    // Check that 'Test' form is available and display a message.
+    $this->drupalGet('/webform/test_form_limit_total_unique/test');
+    $this->assertRaw(' The below webform has been prepopulated with custom/random test data. When submitted, this information <strong>will still be saved</strong> and/or <strong>sent to designated recipients</strong>');
+
+    // Check that name is empty for new submission for root user.
+    $this->drupalLogin($this->rootUser);
+    $this->drupalGet('/webform/test_form_limit_total_unique');
+    $this->assertFieldByName('name', '');
+
+    // Check that name is set to 'John Smith' and 'Submission information' is
+    // visible for admin user.
+    $this->drupalLogin($admin_user);
+    $sid = $this->postSubmission($webform_total_unique, ['name' => 'John Smith']);
+    $this->drupalGet('/webform/test_form_limit_total_unique');
+    $this->assertFieldByName('name', 'John Smith');
+    $this->assertRaw("<div><b>Submission ID:</b> $sid</div>");
+
+    // Check that name is set to 'John Smith' and 'Submission information' is
+    // visible for root user.
+    $this->drupalGet('/webform/test_form_limit_total_unique');
+    $this->assertFieldByName('name', 'John Smith');
+    $this->assertRaw("<div><b>Submission ID:</b> $sid</div>");
+
+    // Check that 'Test' form also has name set to 'John Smith'
+    // and does not display a message.
+    $this->drupalGet('/webform/test_form_limit_total_unique/test');
+    $this->assertFieldByName('name', 'John Smith');
+    $this->assertRaw("<div><b>Submission ID:</b> $sid</div>");
+    $this->assertNoRaw(' The below webform has been prepopulated with custom/random test data. When submitted, this information <strong>will still be saved</strong> and/or <strong>sent to designated recipients</strong>');
+
+    /**************************************************************************/
+    // Total unique. (node)
+    /**************************************************************************/
+
+    // Create webform node.
+    $node_total_unique = $this->createWebformNode('test_form_limit_total_unique');
+
+    $this->drupalLogin($admin_user);
+
+    // Check that name is empty for new submission for admin user.
+    $this->drupalGet('/node/' . $node_total_unique->id());
+    $this->assertFieldByName('name', '');
+
+    // Check that name is set to 'John Lennon' and 'Submission information' is
+    // visible for admin user.
+    $sid = $this->postNodeSubmission($node_total_unique, ['name' => 'John Lennon']);
+    $this->drupalGet('/webform/test_form_limit_total_unique');
+    $this->assertFieldByName('name', 'John Lennon');
+    $this->assertRaw("<div><b>Submission ID:</b> $sid</div>");
+
+    // Check that 'Test' form also has name set to 'John Lennon'
+    // and does not display a message.
+    $this->drupalGet('/node/' . $node_total_unique->id() . '/webform/test');
+    $this->assertFieldByName('name', 'John Lennon');
+    $this->assertRaw("<div><b>Submission ID:</b> $sid</div>");
+
+    // Check that 'Test' form also has name set to 'John Lennon'
+    // and does not display a message for root user.
+    $this->drupalLogin($this->rootUser);
+    $this->drupalGet('/node/' . $node_total_unique->id() . '/webform/test');
+    $this->assertFieldByName('name', 'John Lennon');
+    $this->assertRaw("<div><b>Submission ID:</b> $sid</div>");
+
+    /**************************************************************************/
+    // User unique. (webform)
+    /**************************************************************************/
+
+    // Check that name is empty for new submission for admin user.
+    $this->drupalLogin($admin_user);
+    $this->drupalGet('/webform/test_form_limit_user_unique');
+    $this->assertFieldByName('name', '');
+
+    // Check that 'Test' form is available and display a message.
+    $this->drupalGet('/webform/test_form_limit_user_unique/test');
+    $this->assertRaw(' The below webform has been prepopulated with custom/random test data. When submitted, this information <strong>will still be saved</strong> and/or <strong>sent to designated recipients</strong>');
+
+    // Check that name is empty for new submission for root user.
+    $this->drupalLogin($this->rootUser);
+    $this->drupalGet('/webform/test_form_limit_user_unique');
+    $this->assertFieldByName('name', '');
+
+    // Check that name is set to 'John Smith' and 'Submission information' is
+    // visible for admin user.
+    $this->drupalLogin($admin_user);
+    $sid = $this->postSubmission($webform_user_unique, ['name' => 'John Smith']);
+    $this->drupalGet('/webform/test_form_limit_user_unique');
+    $this->assertFieldByName('name', 'John Smith');
+    $this->assertRaw("<div><b>Submission ID:</b> $sid</div>");
+
+    // Check that 'Test' form also has name set to 'John Smith'
+    // and does not display a message.
+    $this->drupalGet('/webform/test_form_limit_user_unique/test');
+    $this->assertFieldByName('name', 'John Smith');
+    $this->assertRaw("<div><b>Submission ID:</b> $sid</div>");
+
+    /**************************************************************************/
+
+    // Check that name is still empty for new submission for root user.
+    $this->drupalLogin($this->rootUser);
+    $this->drupalGet('/webform/test_form_limit_user_unique');
+    $this->assertFieldByName('name', '');
+
+    // Check that name is set to 'John Smith' and 'Submission information' is
+    // visible for root user.
+    $sid = $this->postSubmission($webform_user_unique, ['name' => 'Jane Doe']);
+    $this->drupalGet('/webform/test_form_limit_user_unique');
+    $this->assertFieldByName('name', 'Jane Doe');
+    $this->assertRaw("<div><b>Submission ID:</b> $sid</div>");
+
+    // Check that 'Test' form also has name set to 'Jane Doe'
+    // and does not display a message.
+    $this->drupalGet('/webform/test_form_limit_user_unique/test');
+    $this->assertFieldByName('name', 'Jane Doe');
+    $this->assertRaw("<div><b>Submission ID:</b> $sid</div>");
+
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/WebformSubmissionFormLimitsTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsLimitsTest.php
similarity index 85%
rename from web/modules/webform/src/Tests/WebformSubmissionFormLimitsTest.php
rename to web/modules/webform/src/Tests/Settings/WebformSettingsLimitsTest.php
index 740df6bf69bd963964e8e6a69b79a1c14293412d..f7326162a49ff0d6a4d29880210c355c7c91e6c1 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionFormLimitsTest.php
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsLimitsTest.php
@@ -1,16 +1,17 @@
 <?php
 
-namespace Drupal\webform\Tests;
+namespace Drupal\webform\Tests\Settings;
 
 use Drupal\user\Entity\Role;
 use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
 
 /**
  * Tests for webform submission form limits.
  *
  * @group Webform
  */
-class WebformSubmissionFormLimitsTest extends WebformTestBase {
+class WebformSettingsLimitsTest extends WebformTestBase {
 
   /**
    * Modules to enable.
@@ -32,9 +33,6 @@ class WebformSubmissionFormLimitsTest extends WebformTestBase {
   public function setUp() {
     parent::setUp();
 
-    // Create users.
-    $this->createUsers();
-
     // Place webform test blocks.
     $this->placeWebformBlocks('webform_test_block_submission_limit');
   }
@@ -43,9 +41,18 @@ public function setUp() {
    * Tests webform submission form limits.
    */
   public function testFormLimits() {
+    $own_submission_user = $this->drupalCreateUser([
+      'view own webform submission',
+      'edit own webform submission',
+      'delete own webform submission',
+      'access webform submission user',
+    ]);
+
     $webform_limit = Webform::load('test_form_limit');
 
-    $this->drupalGet('webform/test_form_limit');
+    /**************************************************************************/
+
+    $this->drupalGet('/webform/test_form_limit');
 
     // Check webform available.
     $this->assertFieldByName('op', 'Submit');
@@ -56,11 +63,11 @@ public function testFormLimits() {
     $this->assertRaw('0 webform submission(s)');
     $this->assertRaw('4 webform limit (every minute)');
 
-    $this->drupalLogin($this->ownWebformSubmissionUser);
+    $this->drupalLogin($own_submission_user);
 
     // Check that draft does not count toward limit.
     $this->postSubmission($webform_limit, [], t('Save Draft'));
-    $this->drupalGet('webform/test_form_limit');
+    $this->drupalGet('/webform/test_form_limit');
     $this->assertFieldByName('op', 'Submit');
     $this->assertRaw('A partially-completed form was found. Please complete the remaining portions.');
     $this->assertNoRaw('You are only allowed to have 1 submission for this webform.');
@@ -71,7 +78,7 @@ public function testFormLimits() {
 
     // Check limit reached and webform not available for authenticated user.
     $sid = $this->postSubmission($webform_limit);
-    $this->drupalGet('webform/test_form_limit');
+    $this->drupalGet('/webform/test_form_limit');
     $this->assertNoFieldByName('op', 'Submit');
     $this->assertRaw('You are only allowed to have 1 submission for this webform.');
 
@@ -105,7 +112,7 @@ public function testFormLimits() {
     $role->save();
 
     // Check webform is still available for anonymous users.
-    $this->drupalGet('webform/test_form_limit');
+    $this->drupalGet('/webform/test_form_limit');
     $this->assertFieldByName('op', 'Submit');
     $this->assertNoRaw('You are only allowed to have 1 submission for this webform.');
 
@@ -118,7 +125,7 @@ public function testFormLimits() {
     $this->assertRaw('3 webform submission(s)');
 
     // Check limit reached and webform not available for anonymous user.
-    $this->drupalGet('webform/test_form_limit');
+    $this->drupalGet('/webform/test_form_limit');
     $this->assertNoFieldByName('op', 'Submit');
     $this->assertRaw('You are only allowed to have 1 submission for this webform.');
 
@@ -134,7 +141,7 @@ public function testFormLimits() {
     $this->drupalLogout();
 
     // Check total limit.
-    $this->drupalGet('webform/test_form_limit');
+    $this->drupalGet('/webform/test_form_limit');
     $this->assertNoFieldByName('op', 'Submit');
     $this->assertRaw('Only 4 submissions are allowed.');
     $this->assertNoRaw('You are only allowed to have 1 submission for this webform.');
@@ -145,7 +152,7 @@ public function testFormLimits() {
 
     // Check admin can still post submissions.
     $this->drupalLogin($this->rootUser);
-    $this->drupalGet('webform/test_form_limit');
+    $this->drupalGet('/webform/test_form_limit');
     $this->assertFieldByName('op', 'Submit');
     $this->assertRaw('Only 4 submissions are allowed.');
     $this->assertRaw('Only submission administrators are allowed to access this webform and create new submissions.');
@@ -159,7 +166,7 @@ public function testFormLimits() {
 
     // Check submission limit blocks are removed because the submission
     // intervals have passed.
-    $this->drupalGet('webform/test_form_limit');
+    $this->drupalGet('/webform/test_form_limit');
     $this->assertRaw('0 user submission(s)');
     $this->assertRaw('0 webform submission(s)');
   }
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsLoginTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsLoginTest.php
deleted file mode 100644
index 54c14f2748ddd41b050d41760f74057bb4b8172b..0000000000000000000000000000000000000000
--- a/web/modules/webform/src/Tests/Settings/WebformSettingsLoginTest.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-
-namespace Drupal\webform\Tests\Settings;
-
-use Drupal\webform\Entity\Webform;
-use Drupal\webform\Tests\WebformTestBase;
-
-/**
- * Tests for login redirect webform and submissions.
- *
- * @group Webform
- */
-class WebformSettingsLoginTest extends WebformTestBase {
-
-  /**
-   * Webforms to load.
-   *
-   * @var array
-   */
-  protected static $testWebforms = ['test_form_login'];
-
-  /**
-   * Tests webform login setting.
-   */
-  public function testLogin() {
-    // Create a webform submission.
-    $this->drupalLogin($this->rootUser);
-    $webform = Webform::load('test_form_login');
-    $sid = $this->postSubmission($webform);
-    $this->drupalLogout();
-
-    // Check form message is displayed.
-    $this->drupalGet('admin/structure/webform/manage/test_form_login');
-    $this->assertRaw('Please login to access <b>Test: Webform: Login</b>.');
-
-    // Check submission message is displayed.
-    $this->drupalGet("admin/structure/webform/manage/test_form_login/submission/$sid");
-    $this->assertRaw("Please login to access <b>Test: Webform: Login: Submission #$sid</b>.");
-
-    // Disable login message.
-    $webform = Webform::load('test_form_login');
-    $webform->setSetting('form_login', FALSE);
-    $webform->setSetting('submission_login', FALSE);
-    $webform->save();
-
-    // Check submission message is not displayed.
-    $this->drupalGet('admin/structure/webform/manage/test_form_login');
-    $this->assertNoRaw('Please login to access <b>Test: Webform: Login</b>.');
-
-    // Check form message is not displayed.
-    $this->drupalGet("admin/structure/webform/manage/test_form_login/submission/$sid");
-    $this->assertNoRaw("Please login to access <b>Test: Webform: Login: Submission #$sid</b>.");
-  }
-
-}
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsPathTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsPathTest.php
index 67cd4653b61e05afa759cc40e856da7f364e2191..787c509df342586c1c11434bfd830a370fce7e49 100644
--- a/web/modules/webform/src/Tests/Settings/WebformSettingsPathTest.php
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsPathTest.php
@@ -14,12 +14,17 @@
  */
 class WebformSettingsPathTest extends WebformTestBase {
 
-  public static $modules = ['path', 'webform'];
+  public static $modules = ['path', 'webform', 'node'];
 
   /**
    * Tests YAML page and title.
    */
   public function testPaths() {
+    /** @var \Drupal\Core\Path\AliasStorageInterface $alias_storage */
+    $alias_storage = $this->container->get('path.alias_storage');
+
+    $node = $this->drupalCreateNode();
+
     $webform = Webform::create([
       'langcode' => 'en',
       'status' => WebformInterface::STATUS_OPEN,
@@ -29,56 +34,95 @@ public function testPaths() {
         'test' => ['#markup' => 'test'],
       ]),
     ]);
+    $webform->setSetting('draft', WebformInterface::DRAFT_ALL);
     $webform->save();
+    $webform_path = '/webform/' . $webform->id();
+    $form_path = '/form/' . str_replace('_', '-', $webform->id());
+
+    // Check paths.
+    $this->drupalLogin($this->rootUser);
+
+    // Check that aliases exist.
+    $this->assert(is_array($alias_storage->load(['alias' => $form_path])));
+    $this->assert(is_array($alias_storage->load(['alias' => "$form_path/confirmation"])));
+    $this->assert(is_array($alias_storage->load(['alias' => "$form_path/drafts"])));
+    $this->assert(is_array($alias_storage->load(['alias' => "$form_path/submissions"])));
 
     // Check default system submit path.
-    $this->drupalGet('webform/' . $webform->id());
+    $this->drupalGet($webform_path);
     $this->assertResponse(200, 'Submit system path exists');
 
     // Check default alias submit path.
-    $this->drupalGet('form/' . str_replace('_', '-', $webform->id()));
+    $this->drupalGet($form_path);
     $this->assertResponse(200, 'Submit URL alias exists');
 
     // Check default alias confirm path.
-    $this->drupalGet('form/' . str_replace('_', '-', $webform->id()) . '/confirmation');
+    $this->drupalGet("$form_path/confirmation");
     $this->assertResponse(200, 'Confirm URL alias exists');
 
-    // Check page hidden (i.e. access denied).
+    // Check default alias drafts path.
+    $this->drupalGet("$form_path/drafts");
+    $this->assertResponse(200, 'Drafts URL alias exists');
+
+    // Check default alias submissions path.
+    $this->drupalGet("$form_path/submissions");
+    $this->assertResponse(200, 'Submissions URL alias exists');
+
+    $this->drupalLogout();
+
+    // Disable paths for the webform.
     $webform->setSettings(['page' => FALSE])->save();
-    $this->drupalGet('webform/' . $webform->id());
+
+    // Check that aliases do not exist.
+    $this->assertFalse($alias_storage->load(['alias' => $form_path]));
+    $this->assertFalse($alias_storage->load(['alias' => "$form_path/confirmation"]));
+    $this->assertFalse($alias_storage->load(['alias' => "$form_path/drafts"]));
+    $this->assertFalse($alias_storage->load(['alias' => "$form_path/submissions"]));
+
+    // Check page hidden (i.e. access denied).
+    $this->drupalGet($webform_path);
     $this->assertResponse(403, 'Submit system path access denied');
     $this->assertNoRaw('Only webform administrators are allowed to access this page and create new submissions.');
-    $this->drupalGet('form/' . str_replace('_', '-', $webform->id()));
+    $this->drupalGet($form_path);
     $this->assertResponse(404, 'Submit URL alias does not exist');
 
+    // Check page hidden with source entity.
+    $this->drupalGet($webform_path, ['query' => ['source_entity_type' => 'node', 'source_entity_id' => $node->id()]]);
+    $this->assertResponse(403, 'Submit system path access denied');
+
+    // Check page visible with source entity.
+    $webform->setSettings(['form_prepopulate_source_entity' => TRUE])->save();
+    $this->drupalGet($webform_path, ['query' => ['source_entity_type' => 'node', 'source_entity_id' => $node->id()]]);
+    $this->assertResponse(200, 'Submit system path exists');
+
     // Check hidden page visible to admin.
     $this->drupalLogin($this->rootUser);
-    $this->drupalGet('webform/' . $webform->id());
+    $this->drupalGet($webform_path);
     $this->assertResponse(200, 'Submit system path access permitted');
     $this->assertRaw('Only webform administrators are allowed to access this page and create new submissions.');
     $this->drupalLogout();
 
     // Check custom submit and confirm path.
     $webform->setSettings(['page' => TRUE, 'page_submit_path' => 'page_submit_path', 'page_confirm_path' => 'page_confirm_path'])->save();
-    $this->drupalGet('page_submit_path');
+    $this->drupalGet('/page_submit_path');
     $this->assertResponse(200, 'Submit system path access permitted');
-    $this->drupalGet('page_confirm_path');
+    $this->drupalGet('/page_confirm_path');
     $this->assertResponse(200, 'Submit URL alias access permitted');
 
     // Check custom base path.
     $webform->setSettings(['page_submit_path' => '', 'page_confirm_path' => ''])->save();
     $this->drupalLogin($this->rootUser);
     $this->drupalPostForm('admin/structure/webform/config', ['page_settings[default_page_base_path]' => 'base/path'], t('Save configuration'));
-    $this->drupalGet('base/path/' . str_replace('_', '-', $webform->id()));
+    $this->drupalGet('/base/path/' . str_replace('_', '-', $webform->id()));
     $this->assertResponse(200, 'Submit URL alias with custom base path exists');
-    $this->drupalGet('base/path/' . str_replace('_', '-', $webform->id()) . '/confirmation');
+    $this->drupalGet('/base/path/' . str_replace('_', '-', $webform->id()) . '/confirmation');
     $this->assertResponse(200, 'Confirm URL alias with custom base path exists');
 
     // Check custom base path delete if accessing webform as page is disabled.
     $webform->setSettings(['page' => FALSE])->save();
-    $this->drupalGet('base/path/' . str_replace('_', '-', $webform->id()));
+    $this->drupalGet('/base/path/' . str_replace('_', '-', $webform->id()));
     $this->assertResponse(404, 'Submit URL alias does not exist.');
-    $this->drupalGet('base/path/' . str_replace('_', '-', $webform->id()) . '/confirmation');
+    $this->drupalGet('/base/path/' . str_replace('_', '-', $webform->id()) . '/confirmation');
     $this->assertResponse(404, 'Confirm URL alias does not exist.');
 
     // Disable automatic generation of paths.
@@ -96,13 +140,15 @@ public function testPaths() {
       ]),
     ]);
     $webform->save();
+    $webform_path = '/webform/' . $webform->id();
+    $form_path = '/form/' . str_replace('_', '-', $webform->id());
 
     // Check default system submit path.
-    $this->drupalGet('webform/' . $webform->id());
+    $this->drupalGet($webform_path);
     $this->assertResponse(200, 'Submit system path exists');
 
     // Check no default alias submit path.
-    $this->drupalGet('form/' . str_replace('_', '-', $webform->id()));
+    $this->drupalGet($form_path);
     $this->assertResponse(404, 'Submit URL alias does not exist');
 
   }
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsPrepopulateTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsPrepopulateTest.php
index ee9eeec7323102a4eebfc6677b183e8c7bb0d2ad..abb3c9ed6d06b89f6811c04bb6be805fde3a6db4 100644
--- a/web/modules/webform/src/Tests/Settings/WebformSettingsPrepopulateTest.php
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsPrepopulateTest.php
@@ -32,13 +32,13 @@ public function testPrepopulate() {
     $webform_prepopulate = Webform::load('test_form_prepopulate');
 
     // Check prepopulation of an element.
-    $this->drupalGet('webform/test_form_prepopulate', ['query' => ['name' => 'John', 'colors' => ['red', 'white']]]);
+    $this->drupalGet('/webform/test_form_prepopulate', ['query' => ['name' => 'John', 'colors' => ['red', 'white']]]);
     $this->assertFieldByName('name', 'John');
     $this->assertFieldChecked('edit-colors-red');
     $this->assertFieldChecked('edit-colors-white');
     $this->assertNoFieldChecked('edit-colors-blue');
 
-    $this->drupalGet('webform/test_form_prepopulate', ['query' => ['name' => 'John', 'colors' => 'red']]);
+    $this->drupalGet('/webform/test_form_prepopulate', ['query' => ['name' => 'John', 'colors' => 'red']]);
     $this->assertFieldByName('name', 'John');
     $this->assertFieldChecked('edit-colors-red');
     $this->assertNoFieldChecked('edit-colors-white');
@@ -47,7 +47,7 @@ public function testPrepopulate() {
     // Check disabling prepopulation of an element.
     $webform_prepopulate->setSetting('form_prepopulate', FALSE);
     $webform_prepopulate->save();
-    $this->drupalGet('webform/test_form_prepopulate', ['query' => ['name' => 'John']]);
+    $this->drupalGet('/webform/test_form_prepopulate', ['query' => ['name' => 'John']]);
     $this->assertFieldByName('name', '');
 
     /**************************************************************************/
@@ -79,25 +79,32 @@ public function testPrepopulate() {
 
     // Check required prepopulated source entity displays error when no source
     // entity is defined.
-    $this->drupalGet('webform/test_form_prepopulate');
+    $this->drupalGet('/webform/test_form_prepopulate');
     $this->assertRaw('This webform is not available. Please contact the site administrator.');
 
     // Check required prepopulated source entity displays error when invalid
     // source entity is defined.
-    $this->drupalGet('webform/test_form_prepopulate', ['query' => ['source_entity_type' => 'webform', 'source_entity_id' => 'DOES_NOT_EXIST']]);
+    $this->drupalGet('/webform/test_form_prepopulate', ['query' => ['source_entity_type' => 'webform', 'source_entity_id' => 'DOES_NOT_EXIST']]);
     $this->assertRaw('This webform is not available. Please contact the site administrator.');
 
     // Check required prepopulated source entity loads when source entity is
     // valid.
-    $this->drupalGet('webform/test_form_prepopulate', ['query' => ['source_entity_type' => 'webform', 'source_entity_id' => 'contact']]);
+    $this->drupalGet('/webform/test_form_prepopulate', ['query' => ['source_entity_type' => 'webform', 'source_entity_id' => 'contact']]);
     $this->assertNoRaw('This webform is not available. Please contact the site administrator.');
 
+    // Check that required prepopulated source entity can be updated (edit).
+    $this->drupalLogin($this->rootUser);
+    $sid = $this->postSubmission($webform_prepopulate, [], t('Submit'), ['query' => ['source_entity_type' => 'webform', 'source_entity_id' => 'contact']]);
+    $this->drupalGet("/admin/structure/webform/manage/test_form_prepopulate/submission/$sid/edit");
+    $this->assertNoRaw('This webform is not available. Please contact the site administrator.');
+    $this->drupalLogout();
+
     // Set prepopulated source entity type to user.
     $webform_prepopulate->setSetting('form_prepopulate_source_entity_type', 'user');
     $webform_prepopulate->save();
 
     // Check invalid source entity type displays error.
-    $this->drupalGet('webform/test_form_prepopulate', ['query' => ['source_entity_type' => 'webform', 'source_entity_id' => 'contact']]);
+    $this->drupalGet('/webform/test_form_prepopulate', ['query' => ['source_entity_type' => 'webform', 'source_entity_id' => 'contact']]);
     $this->assertRaw('This webform is not available. Please contact the site administrator.');
 
     // Set prepopulated source entity type to webform.
@@ -105,7 +112,7 @@ public function testPrepopulate() {
     $webform_prepopulate->save();
 
     // Check invalid source entity type displays error.
-    $this->drupalGet('webform/test_form_prepopulate', ['query' => ['source_entity_type' => 'webform', 'source_entity_id' => 'contact']]);
+    $this->drupalGet('/webform/test_form_prepopulate', ['query' => ['source_entity_type' => 'webform', 'source_entity_id' => 'contact']]);
     $this->assertNoRaw('This webform is not available. Please contact the site administrator.');
   }
 
diff --git a/web/modules/webform/src/Tests/WebformSubmissionFormPreviewTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsPreviewTest.php
similarity index 54%
rename from web/modules/webform/src/Tests/WebformSubmissionFormPreviewTest.php
rename to web/modules/webform/src/Tests/Settings/WebformSettingsPreviewTest.php
index 7248837344ee53f901c2767429017a6cd3179c7c..9ac5fbfb564ca9f7641ff76d430d7b1678b6cc18 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionFormPreviewTest.php
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsPreviewTest.php
@@ -1,15 +1,16 @@
 <?php
 
-namespace Drupal\webform\Tests;
+namespace Drupal\webform\Tests\Settings;
 
 use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
 
 /**
  * Tests for webform submission form preview.
  *
  * @group Webform
  */
-class WebformSubmissionFormPreviewTest extends WebformTestBase {
+class WebformSettingsPreviewTest extends WebformTestBase {
 
   /**
    * Webforms to load.
@@ -40,24 +41,55 @@ public function testPreview() {
     $webform_preview = Webform::load('test_form_preview');
 
     // Check webform with optional preview.
-    $this->drupalGet('webform/test_form_preview');
+    $this->drupalGet('/webform/test_form_preview');
     $this->assertFieldByName('op', 'Submit');
     $this->assertFieldByName('op', 'Preview');
 
-    // Check default preview.
-    $this->drupalPostForm('webform/test_form_preview', ['name' => 'test', 'email' => 'example@example.com'], t('Preview'));
+    // Check default preview with values.
+    $this->drupalPostForm('webform/test_form_preview', ['name' => 'test', 'email' => 'example@example.com', 'checkbox' => TRUE], t('Preview'));
 
     $this->assertRaw('<h1 class="page-title">Test: Webform: Preview: Preview</h1>');
+
     $this->assertRaw('<b>Preview</b></li>');
+
     $this->assertRaw('Please review your submission. Your submission is not complete until you press the "Submit" button!');
+
     $this->assertFieldByName('op', 'Submit');
     $this->assertFieldByName('op', '< Previous');
-    $this->assertRaw('<div id="test_form_preview--name" class="webform-element webform-element-type-textfield js-form-item form-item js-form-type-item form-type-item js-form-item-name form-item-name">');
+
+    $this->assertRaw('<div class="webform-preview js-form-wrapper form-wrapper" data-drupal-selector="edit-preview" id="edit-preview"><fieldset class="format-attributes-class webform-container webform-container-type-fieldset js-form-item form-item js-form-wrapper form-wrapper" id="test_form_preview--fieldset">');
+    $this->assertRaw('<div class="format-attributes-class webform-element webform-element-type-textfield js-form-item form-item js-form-type-item form-type-item js-form-item-name form-item-name" id="test_form_preview--name">');
     $this->assertRaw('<label>Name</label>' . PHP_EOL . '        test');
-    $this->assertRaw('<div id="test_form_preview--email" class="webform-element webform-element-type-email js-form-item form-item js-form-type-item form-type-item js-form-item-email form-item-email">');
+
+    $this->assertRaw('<section class="format-attributes-class js-form-item form-item js-form-wrapper form-wrapper webform-section" id="test_form_preview--container">');
+    $this->assertRaw('<div class="format-attributes-class webform-element webform-element-type-email js-form-item form-item js-form-type-item form-type-item js-form-item-email form-item-email" id="test_form_preview--email">');
     $this->assertRaw('<label>Email</label>' . PHP_EOL . '        <a href="mailto:example@example.com">example@example.com</a>');
+
+    $this->assertRaw('<div class="format-attributes-class webform-element webform-element-type-checkbox js-form-item form-item js-form-type-item form-type-item js-form-item-checkbox form-item-checkbox" id="test_form_preview--checkbox">');
+    $this->assertRaw('<section class="format-attributes-class js-form-item form-item js-form-wrapper form-wrapper webform-section" id="test_form_preview--section">');
+    $this->assertRaw('<label>Checkbox</label>' . PHP_EOL . '        Yes');
     $this->assertRaw('<div class="webform-preview js-form-wrapper form-wrapper" data-drupal-selector="edit-preview" id="edit-preview">');
 
+    // Check default preview without values.
+    $this->drupalPostForm('webform/test_form_preview', [], t('Preview'));
+    $this->assertNoRaw('<label>Name</label>');
+    $this->assertNoRaw('<label>Email</label>');
+    $this->assertNoRaw('<label>Checkbox</label>');
+
+    // Check submission view without values.
+    $sid = $this->postSubmission($webform_preview);
+    $this->drupalGet("admin/structure/webform/manage/test_form_preview/submission/$sid");
+    $this->assertNoRaw('<label>Name</label>');
+    $this->assertNoRaw('<label>Email</label>');
+    $this->assertNoRaw('<label>Checkbox</label>');
+
+    // Check submission table without values.
+    $this->drupalGet("admin/structure/webform/manage/test_form_preview/submission/$sid/table");
+    $this->assertNoRaw('<th>Name</th>');
+    $this->assertNoRaw('<th>Email</th>');
+    $this->assertNoRaw('<th>Checkbox</th>');
+    $this->assertNoRaw('<td>No</td>');
+
     // Clear default preview message.
     \Drupal::configFactory()->getEditable('webform.settings')
       ->set('settings.default_preview_message', '')
@@ -67,22 +99,40 @@ public function testPreview() {
     $this->drupalPostForm('webform/test_form_preview', ['name' => 'test', 'email' => 'example@example.com'], t('Preview'));
     $this->assertNoRaw('Please review your submission. Your submission is not complete until you press the "Submit" button!');
 
-    // Set preview to include empty.
+    // Set preview and submission to include empty.
     $webform_preview->setSetting('preview_exclude_empty', FALSE);
+    $webform_preview->setSetting('preview_exclude_empty_checkbox', FALSE);
+    $webform_preview->setSetting('submission_exclude_empty', FALSE);
+    $webform_preview->setSetting('submission_exclude_empty_checkbox', FALSE);
     $webform_preview->save();
 
-    // Check empty element is included from preview.
-    $this->drupalPostForm('webform/test_form_preview', ['name' => '', 'email' => ''], t('Preview'));
+    // Check empty elements are included in preview.
+    $this->drupalPostForm('webform/test_form_preview', ['name' => '', 'email' => '', 'checkbox' => FALSE], t('Preview'));
     $this->assertRaw('<label>Name</label>' . PHP_EOL . '        {Empty}');
-    $this->assertRaw('<div id="test_form_preview--email" class="webform-element webform-element-type-email js-form-item form-item js-form-type-item form-type-item js-form-item-email form-item-email">');
+    $this->assertRaw('<div class="format-attributes-class webform-element webform-element-type-email js-form-item form-item js-form-type-item form-type-item js-form-item-email form-item-email" id="test_form_preview--email">');
     $this->assertRaw('<label>Email</label>' . PHP_EOL . '        {Empty}');
+    $this->assertRaw('<label>Checkbox</label>' . PHP_EOL . '        No');
+
+    // Check empty elements are included in submission view.
+    $sid = $this->postSubmission($webform_preview);
+    $this->drupalGet("admin/structure/webform/manage/test_form_preview/submission/$sid");
+    $this->assertRaw('<label>Name</label>');
+    $this->assertRaw('<label>Email</label>');
+    $this->assertRaw('<label>Checkbox</label>');
+
+    // Check submission table without values.
+    $this->drupalGet("admin/structure/webform/manage/test_form_preview/submission/$sid/table");
+    $this->assertRaw('<th>Name</th>');
+    $this->assertRaw('<th>Email</th>');
+    $this->assertRaw('<th>Checkbox</th>');
+    $this->assertRaw('<td>No</td>');
 
     // Add special character to title.
     $webform_preview->set('title', "This has special characters. '<>\"&");
     $webform_preview->save();
 
     // Check special characters in form page title.
-    $this->drupalGet('webform/test_form_preview');
+    $this->drupalGet('/webform/test_form_preview');
     $this->assertRaw('<title>This has special characters. \'"& | Drupal</title>');
     $this->assertRaw('<h1 class="page-title">This has special characters. &#039;&lt;&gt;&quot;&amp;</h1>');
 
@@ -120,7 +170,7 @@ public function testPreview() {
     $this->assertNoRaw('<label>Email</label>');
     $this->assertRaw('<div class="preview-custom webform-preview js-form-wrapper form-wrapper" data-drupal-selector="edit-preview" id="edit-preview">');
 
-    $this->drupalGet('webform/test_form_preview');
+    $this->drupalGet('/webform/test_form_preview');
     $this->assertNoFieldByName('op', 'Submit');
     $this->assertFieldByName('op', '{Preview}');
 
@@ -128,7 +178,6 @@ public function testPreview() {
     $this->drupalPostForm('webform/test_form_preview', ['name' => 'test', 'email' => ''], t('{Preview}'));
     $this->assertRaw('<label>Name</label>' . PHP_EOL . '        test');
     $this->assertNoRaw('<label>Email</label>');
-
   }
 
 }
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsPreviousTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsPreviousTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..800ad06ca4e857d397b998a1c849e8c411265d18
--- /dev/null
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsPreviousTest.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\webform\Tests\Settings;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
+
+/**
+ * Tests for webform submission form previous.
+ *
+ * @group Webform
+ */
+class WebformSettingsPreviousTest extends WebformTestBase {
+
+  /**
+   * Test webform submission form previous submission(s).
+   */
+  public function testPrevious() {
+    global $base_path;
+
+    $this->drupalLogin($this->rootUser);
+
+    $webform = Webform::load('contact');
+
+    /**************************************************************************/
+    // Previous submission message.
+    /**************************************************************************/
+
+    // Create single submission.
+    $sid_1 = $this->postSubmissionTest($webform);
+
+    // Check default global previous submission message.
+    $this->drupalGet('/webform/contact');
+    $this->assertRaw("You have already submitted this webform. <a href=\"{$base_path}webform/contact/submissions/{$sid_1}\">View your previous submission</a>.");
+
+    // Check custom global previous submission message.
+    $this->config('webform.settings')
+      ->set('settings.default_previous_submission_message', '{default_previous_submission}')
+      ->save();
+    $this->drupalGet('/webform/contact');
+    $this->assertRaw('{default_previous_submission}');
+
+    // Check custom webform previous submission message.
+    $webform
+      ->setSetting('previous_submission_message', '{custom_previous_submission}')
+      ->save();
+    $this->drupalGet('/webform/contact');
+    $this->assertRaw('{custom_previous_submission}');
+
+    /**************************************************************************/
+    // Previous submissions message.
+    /**************************************************************************/
+
+    // Create second submission.
+    $sid_2 = $this->postSubmissionTest($webform);
+
+    // Check default global previous submissions message.
+    $this->drupalGet('/webform/contact');
+    $this->assertRaw("You have already submitted this webform. <a href=\"{$base_path}webform/contact/submissions\">View your previous submissions</a>.");
+
+    // Check custom global previous submissions message.
+    $this->config('webform.settings')
+      ->set('settings.default_previous_submissions_message', '{default_previous_submissions}')
+      ->save();
+    $this->drupalGet('/webform/contact');
+    $this->assertRaw('{default_previous_submissions}');
+
+    // Check custom webform previous submissions message.
+    $webform
+      ->setSetting('previous_submissions_message', '{custom_previous_submissions}')
+      ->save();
+    $this->drupalGet('/webform/contact');
+    $this->assertRaw('{custom_previous_submissions}');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsRemoteAddrTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsRemoteAddrTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..81c70d495b9a6c5e4f0e87fe725077b6b6b4a48e
--- /dev/null
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsRemoteAddrTest.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\webform\Tests\Settings;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Entity\WebformSubmission;
+use Drupal\webform\Tests\WebformTestBase;
+
+/**
+ * Tests for disable tracking of remote IP address.
+ *
+ * @group Webform
+ */
+class WebformSettingsRemoteAddrTest extends WebformTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_form_remote_addr'];
+
+  /**
+   * Tests webform disable remote IP address.
+   */
+  public function testRemoteAddr() {
+    $this->drupalLogin($this->rootUser);
+
+    $webform = Webform::load('test_form_remote_addr');
+    $sid = $this->postSubmission($webform, ['name' => 'John']);
+    $webform_submission = WebformSubmission::load($sid);
+    $this->assertEqual($webform_submission->getRemoteAddr(), t('(unknown)'));
+    $this->assertEqual($webform_submission->getOwnerId(), 1);
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsScheduleTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsScheduleTest.php
index 03fb814aa3d7c39cabcd3737a9f958a9781c6580..03cc76dfb93fb32fa0ad43a84f82e07f2d874579 100644
--- a/web/modules/webform/src/Tests/Settings/WebformSettingsScheduleTest.php
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsScheduleTest.php
@@ -38,21 +38,21 @@ public function testSchedule() {
     // Check webform open message is displayed.
     $this->assertTrue($webform_opening->isClosed());
     $this->assertTrue($webform_opening->isOpening());
-    $this->drupalGet('webform/test_form_opening');
+    $this->drupalGet('/webform/test_form_opening');
     $this->assertNoRaw('This message should not be displayed)');
     $this->assertRaw('This form is opening soon.');
 
     // Check webform closed message is displayed.
     $webform_opening->setSetting('form_open_message', '');
     $webform_opening->save();
-    $this->drupalGet('webform/test_form_opening');
+    $this->drupalGet('/webform/test_form_opening');
     $this->assertNoRaw('This form is opening soon.');
     $this->assertRaw('This form has not yet been opened to submissions.');
 
     $this->drupalLogin($this->rootUser);
 
     // Check webform is not closed for admins and warning is displayed.
-    $this->drupalGet('webform/test_form_opening');
+    $this->drupalGet('/webform/test_form_opening');
     $this->assertRaw('This message should not be displayed');
     $this->assertNoRaw('This form has not yet been opened to submissions.');
     $this->assertRaw('Only submission administrators are allowed to access this webform and create new submissions.');
@@ -62,7 +62,7 @@ public function testSchedule() {
     $webform_opening->save();
     $this->assertFalse($webform_opening->isClosed());
     $this->assertTrue($webform_opening->isOpen());
-    $this->drupalGet('webform/test_form_opening');
+    $this->drupalGet('/webform/test_form_opening');
     $this->assertRaw('This message should not be displayed');
     $this->assertNoRaw('This form has not yet been opened to submissions.');
     $this->assertNoRaw('Only submission administrators are allowed to access this webform and create new submissions.');
@@ -78,21 +78,21 @@ public function testSchedule() {
     // Check webform closed message is displayed.
     $this->assertTrue($webform_closed->isClosed());
     $this->assertFalse($webform_closed->isOpen());
-    $this->drupalGet('webform/test_form_closed');
+    $this->drupalGet('/webform/test_form_closed');
     $this->assertNoRaw('This message should not be displayed)');
     $this->assertRaw('This form is closed.');
 
     // Check webform closed message is displayed.
     $webform_closed->setSetting('form_close_message', '');
     $webform_closed->save();
-    $this->drupalGet('webform/test_form_closed');
+    $this->drupalGet('/webform/test_form_closed');
     $this->assertNoRaw('This form is closed.');
-    $this->assertRaw('Sorry...This form is closed to new submissions.');
+    $this->assertRaw('Sorry…This form is closed to new submissions.');
 
     $this->drupalLogin($this->rootUser);
 
     // Check webform is not closed for admins and warning is displayed.
-    $this->drupalGet('webform/test_form_closed');
+    $this->drupalGet('/webform/test_form_closed');
     $this->assertRaw('This message should not be displayed');
     $this->assertNoRaw('This form is closed.');
     $this->assertRaw('Only submission administrators are allowed to access this webform and create new submissions.');
@@ -102,7 +102,7 @@ public function testSchedule() {
     $webform_closed->save();
     $this->assertFalse($webform_closed->isClosed());
     $this->assertTrue($webform_closed->isOpen());
-    $this->drupalGet('webform/test_form_closed');
+    $this->drupalGet('/webform/test_form_closed');
     $this->assertRaw('This message should not be displayed');
     $this->assertNoRaw('This form is closed.');
     $this->assertNoRaw('Only submission administrators are allowed to access this webform and create new submissions.');
diff --git a/web/modules/webform/src/Tests/Settings/WebformSettingsStatusTest.php b/web/modules/webform/src/Tests/Settings/WebformSettingsStatusTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c5a6f9642b8922e6de9a0878648190fe53a3e25b
--- /dev/null
+++ b/web/modules/webform/src/Tests/Settings/WebformSettingsStatusTest.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\webform\Tests\Settings;
+
+use Drupal\webform\Tests\WebformTestBase;
+use Drupal\webform\WebformInterface;
+
+/**
+ * Tests for webform default status.
+ *
+ * @group Webform
+ */
+class WebformSettingsStatusTest extends WebformTestBase {
+
+  /**
+   * Tests default status.
+   */
+  public function testStatus() {
+    $this->drupalLogin($this->rootUser);
+
+    // Check add form status = open.
+    $this->drupalGet('/admin/structure/webform/add');
+    $this->assertFieldChecked('edit-status-open');
+    $this->assertNoFieldChecked('edit-status-closed');
+
+    // Check duplicate form status = open.
+    $this->drupalGet('/admin/structure/webform/manage/contact/duplicate');
+    $this->assertFieldChecked('edit-status-open');
+    $this->assertNoFieldChecked('edit-status-closed');
+
+    // Set default status to closed.
+    $this->config('webform.settings')
+      ->set('settings.default_status', WebformInterface::STATUS_CLOSED)
+      ->save();
+
+    // Check add form status = closed.
+    $this->drupalGet('/admin/structure/webform/add');
+    $this->assertNoFieldChecked('edit-status-open');
+    $this->assertFieldChecked('edit-status-closed');
+
+    // Check duplicate form status = closed.
+    $this->drupalGet('/admin/structure/webform/manage/contact/duplicate');
+    $this->assertNoFieldChecked('edit-status-open');
+    $this->assertFieldChecked('edit-status-closed');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/States/WebformStatesPreviewTest.php b/web/modules/webform/src/Tests/States/WebformStatesPreviewTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..b0c69904a5ca07b4a22c51d8dac89ea488f70321
--- /dev/null
+++ b/web/modules/webform/src/Tests/States/WebformStatesPreviewTest.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Drupal\webform\Tests\States;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
+
+/**
+ * Tests for webform states preview.
+ *
+ * @group Webform
+ */
+class WebformStatesPreviewTest extends WebformTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = [
+    'test_states_server_preview',
+    'test_states_server_save',
+    'test_states_server_clear',
+  ];
+
+  /**
+   * Tests visible conditions (#states) validator for elements .
+   */
+  public function testStatesValidatorElementVisible() {
+    $webform_preview = Webform::load('test_states_server_preview');
+
+    // Check trigger unchecked and elements are conditionally hidden.
+    $this->postSubmission($webform_preview, [], t('Preview'));
+    $this->assertRaw('trigger_checkbox');
+    $this->assertNoRaw('dependent_checkbox');
+    $this->assertNoRaw('dependent_markup');
+    $this->assertNoRaw('dependent_message');
+    $this->assertNoRaw('dependent_fieldset');
+    $this->assertNoRaw('nested_textfield');
+
+    // Check trigger checked and elements are conditionally visible.
+    $this->postSubmission($webform_preview, ['trigger_checkbox' => TRUE], t('Preview'));
+    $this->assertRaw('trigger_checkbox');
+    $this->assertRaw('dependent_checkbox');
+    $this->assertRaw('dependent_markup');
+    $this->assertRaw('dependent_message');
+    $this->assertRaw('dependent_fieldset');
+    $this->assertRaw('nested_textfield');
+
+    $webform_save = Webform::load('test_states_server_save');
+
+    // Check trigger unchecked and saved.
+    $this->postSubmission($webform_save, ['trigger_checkbox' => FALSE], t('Submit'));
+    $this->assertRaw("trigger_checkbox: 0
+dependent_hidden: ''
+dependent_checkbox: ''
+dependent_value: ''
+dependent_textfield: ''
+dependent_textfield_multiple: {  }
+dependent_details_textfield: ''");
+
+    // Check trigger checked and saved.
+    $this->postSubmission($webform_save, ['trigger_checkbox' => TRUE], t('Submit'));
+    $this->assertRaw("trigger_checkbox: 1
+dependent_hidden: '{dependent_hidden}'
+dependent_checkbox: 0
+dependent_value: '{value}'
+dependent_textfield: '{dependent_textfield}'
+dependent_textfield_multiple:
+  - '{dependent_textfield}'
+dependent_details_textfield: '{dependent_details_textfield}'");
+
+    $webform_clear = Webform::load('test_states_server_clear');
+
+    // Check trigger unchecked and not cleared.
+    $this->postSubmission($webform_clear, ['trigger_checkbox' => FALSE], t('Submit'));
+    $this->assertRaw("trigger_checkbox: 0
+dependent_hidden: '{dependent_hidden}'
+dependent_checkbox: 1
+dependent_radios: One
+dependent_value: '{value}'
+dependent_textfield: '{dependent_textfield}'
+dependent_textfield_multiple:
+  - '{dependent_textfield}'
+dependent_webform_name:
+  - title: ''
+    first: John
+    middle: ''
+    last: Smith
+    suffix: ''
+    degree: ''
+dependent_details_textfield: '{dependent_details_textfield}'");
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/WebformSubmissionConditionsValidatorTest.php b/web/modules/webform/src/Tests/States/WebformStatesServerTest.php
similarity index 60%
rename from web/modules/webform/src/Tests/WebformSubmissionConditionsValidatorTest.php
rename to web/modules/webform/src/Tests/States/WebformStatesServerTest.php
index b86e0cf367e74f6ccc8fc0a675b263dad226bcee..d6ed19c4e3105cef75f322cfc1f0a53bd77930a3 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionConditionsValidatorTest.php
+++ b/web/modules/webform/src/Tests/States/WebformStatesServerTest.php
@@ -1,16 +1,17 @@
 <?php
 
-namespace Drupal\webform\Tests;
+namespace Drupal\webform\Tests\States;
 
 use Drupal\webform\Element\WebformOtherBase;
 use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
 
 /**
  * Tests for webform submission conditions (#states) validator.
  *
  * @group Webform
  */
-class WebformSubmissionConditionsValidatorTest extends WebformTestBase {
+class WebformStatesServerTest extends WebformTestBase {
 
   /**
    * Webforms to load.
@@ -18,13 +19,13 @@ class WebformSubmissionConditionsValidatorTest extends WebformTestBase {
    * @var array
    */
   protected static $testWebforms = [
-    'test_form_states_server_custom',
-    'test_form_states_server_comp',
-    'test_form_states_server_multiple',
-    'test_form_states_server_nested',
-    'test_form_states_server_preview',
-    'test_form_states_server_required',
-    'test_form_states_server_wizard',
+    'test_states_crosspage',
+    'test_states_server_custom',
+    'test_states_server_comp',
+    'test_states_server_nested',
+    'test_states_server_multiple',
+    'test_states_server_containers',
+    'test_states_server_required',
   ];
 
   /**
@@ -50,7 +51,12 @@ public function setUp() {
    * Tests webform submission conditions (#states) validator required.
    */
   public function testFormStatesValidatorRequired() {
-    $webform = Webform::load('test_form_states_server_required');
+
+    /**************************************************************************/
+    // required.
+    /**************************************************************************/
+
+    $webform = Webform::load('test_states_server_required');
 
     // Check no #states required errors.
     $this->postSubmission($webform);
@@ -107,14 +113,21 @@ public function testFormStatesValidatorRequired() {
     $this->assertRaw('required_hidden_dependent_required field is required.');
 
     /**************************************************************************/
-    // minlength_hidden_trigger
+    // minlength_hidden_trigger.
     /**************************************************************************/
 
     $edit = [
       'minlength_hidden_trigger' => TRUE,
     ];
     $this->postSubmission($webform, $edit);
-    $this->assertRaw('<em class="placeholder">minlength_hidden_dependent</em> cannot be less than <em class="placeholder">1</em> characters but is currently <em class="placeholder">0</em> characters long.');
+    $this->assertNoRaw('<em class="placeholder">minlength_hidden_dependent</em> cannot be less than <em class="placeholder">5</em> characters');
+
+    $edit = [
+      'minlength_hidden_trigger' => TRUE,
+      'minlength_hidden_dependent' => 'X',
+    ];
+    $this->postSubmission($webform, $edit);
+    // $this->assertRaw('<em class="placeholder">minlength_hidden_dependent</em> cannot be less than <em class="placeholder">5</em> characters');
 
     /**************************************************************************/
     // checkboxes_trigger.
@@ -127,6 +140,25 @@ public function testFormStatesValidatorRequired() {
     $this->postSubmission($webform, $edit);
     $this->assertRaw('checkboxes_dependent_required field is required.');
 
+    /**************************************************************************/
+    // checkboxes_other_trigger.
+    /**************************************************************************/
+
+    // Check required checkboxes other checkbox.
+    $edit = [
+      'checkboxes_other_trigger[checkboxes][one]' => TRUE,
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('checkboxes_other_dependent_required field is required.');
+
+    // Check required checkboxes other text field.
+    $edit = [
+      'checkboxes_other_trigger[checkboxes][_other_]' => TRUE,
+      'checkboxes_other_trigger[other]' => 'filled',
+    ];
+    $this->postSubmission($webform, $edit);
+    $this->assertRaw('checkboxes_other_dependent_required field is required.');
+
     /**************************************************************************/
     // text_format_trigger.
     /**************************************************************************/
@@ -300,7 +332,7 @@ public function testFormStatesValidatorRequired() {
     // custom.
     /**************************************************************************/
 
-    $webform = Webform::load('test_form_states_server_custom');
+    $webform = Webform::load('test_states_server_custom');
 
     // Check no #states required errors.
     $this->postSubmission($webform);
@@ -323,7 +355,7 @@ public function testFormStatesValidatorRequired() {
     // multiple element.
     /**************************************************************************/
 
-    $webform = Webform::load('test_form_states_server_multiple');
+    $webform = Webform::load('test_states_server_multiple');
 
     $edit = [
       'trigger_required' => TRUE,
@@ -337,18 +369,19 @@ public function testFormStatesValidatorRequired() {
     // composite element.
     /**************************************************************************/
 
-    $webform = Webform::load('test_form_states_server_comp');
+    $webform = Webform::load('test_states_server_comp');
 
     $edit = [
       'webform_name_trigger' => TRUE,
       'webform_name_multiple_trigger' => TRUE,
       'webform_name_multiple_header_trigger' => TRUE,
+      'webform_name_nested_trigger' => TRUE,
     ];
     $this->postSubmission($webform, $edit);
 
     // Check basic composite.
     $this->assertRaw('First field is required.');
-    $this->assertRaw('<input data-drupal-selector="edit-webform-name-first" type="text" id="edit-webform-name-first" name="webform_name[first]" value="" size="60" maxlength="255" class="form-text error" aria-invalid="true" data-drupal-states="{&quot;required&quot;:{&quot;:input[name=\u0022webform_name_trigger\u0022]&quot;:{&quot;checked&quot;:true}}}" />');
+    $this->assertRaw('<input data-drupal-selector="edit-webform-name-first" type="text" id="edit-webform-name-first" name="webform_name[first]" value="" size="60" maxlength="255" class="form-text error" aria-invalid="true" data-drupal-states="{&quot;required&quot;:{&quot;.webform-submission-test-states-server-comp-add-form :input[name=\u0022webform_name_trigger\u0022]&quot;:{&quot;checked&quot;:true}}}" />');
 
     // Check multiple composite with custom error.
     $this->assertRaw("Custom error message for &#039;last&#039; element.");
@@ -356,20 +389,26 @@ public function testFormStatesValidatorRequired() {
 
     // Check multiple table composite.
     $this->assertRaw('Last field is required.');
-    $this->assertRaw('<input data-drupal-selector="edit-webform-name-multiple-header-items-0-last" type="text" id="edit-webform-name-multiple-header-items-0-last" name="webform_name_multiple_header[items][0][last]" value="" size="60" maxlength="255" class="form-text error" aria-invalid="true" data-drupal-states="{&quot;required&quot;:{&quot;:input[name=\u0022webform_name_multiple_header_trigger\u0022]&quot;:{&quot;checked&quot;:true}}}" />');
+    $this->assertRaw('<input data-drupal-selector="edit-webform-name-multiple-header-items-0-last" type="text" id="edit-webform-name-multiple-header-items-0-last" name="webform_name_multiple_header[items][0][last]" value="" size="60" maxlength="255" class="form-text error" aria-invalid="true" data-drupal-states="{&quot;required&quot;:{&quot;.webform-submission-test-states-server-comp-add-form :input[name=\u0022webform_name_multiple_header_trigger\u0022]&quot;:{&quot;checked&quot;:true}}}" />');
+
+    // Check single nested composite.
+    $this->assertRaw('webform_name_nested_first field is required.');
+    $this->assertRaw('webform_name_nested_last field is required.');
+    $this->assertRaw(' <input data-drupal-selector="edit-webform-name-nested-last" type="text" id="edit-webform-name-nested-last" name="webform_name_nested[last]" value="" size="60" maxlength="255" class="form-text error" aria-invalid="true" data-drupal-states="{&quot;required&quot;:{&quot;.webform-submission-test-states-server-comp-add-form :input[name=\u0022webform_name_nested_trigger\u0022]&quot;:{&quot;checked&quot;:true}}}" />');
 
     /**************************************************************************/
-    // nested.
+    // nested containers.
     /**************************************************************************/
 
-    $webform = Webform::load('test_form_states_server_nested');
+    $webform = Webform::load('test_states_server_containers');
 
     // Check sub elements.
-    $this->drupalGet('webform/test_form_states_server_nested');
-    $this->assertRaw('<input data-drupal-selector="edit-visible-textfield" type="text" id="edit-visible-textfield" name="visible_textfield" value="" size="60" maxlength="255" class="form-text" data-drupal-states="{&quot;required&quot;:{&quot;:input[name=\u0022visible_trigger\u0022]&quot;:{&quot;checked&quot;:true}}}" />');
-    $this->assertRaw('<input data-drupal-selector="edit-visible-custom-textfield" type="text" id="edit-visible-custom-textfield" name="visible_custom_textfield" value="" size="60" maxlength="255" class="form-text" data-drupal-states="{&quot;required&quot;:{&quot;:input[name=\u0022visible_trigger\u0022]&quot;:{&quot;checked&quot;:true},&quot;:input[name=\u0022visible_textfield\u0022]&quot;:{&quot;filled&quot;:true}}}" />');
-    $this->assertRaw('<input data-drupal-selector="edit-visible-slide-textfield" type="text" id="edit-visible-slide-textfield" name="visible_slide_textfield" value="" size="60" maxlength="255" class="form-text" data-drupal-states="{&quot;required&quot;:{&quot;:input[name=\u0022visible_trigger\u0022]&quot;:{&quot;checked&quot;:true}}}" />');
-    $this->assertRaw('<input data-drupal-selector="edit-visible-slide-custom-textfield" type="text" id="edit-visible-slide-custom-textfield" name="visible_slide_custom_textfield" value="" size="60" maxlength="255" class="form-text" data-drupal-states="{&quot;required&quot;:{&quot;:input[name=\u0022visible_trigger\u0022]&quot;:{&quot;checked&quot;:true},&quot;:input[name=\u0022visible_slide_textfield\u0022]&quot;:{&quot;filled&quot;:true}}}" />');
+    $this->drupalGet('/webform/test_states_server_containers');
+    $this->assertRaw('<input data-drupal-selector="edit-visible-textfield" type="text" id="edit-visible-textfield" name="visible_textfield" value="" size="60" maxlength="255" class="form-text" data-drupal-states="{&quot;required&quot;:{&quot;.webform-submission-test-states-server-containers-add-form :input[name=\u0022visible_trigger\u0022]&quot;:{&quot;checked&quot;:true}}}" />');
+    $this->assertRaw('<input data-drupal-selector="edit-visible-custom-textfield" type="text" id="edit-visible-custom-textfield" name="visible_custom_textfield" value="" size="60" maxlength="255" class="form-text" data-drupal-states="{&quot;required&quot;:{&quot;.webform-submission-test-states-server-containers-add-form :input[name=\u0022visible_trigger\u0022]&quot;:{&quot;checked&quot;:true},&quot;.webform-submission-test-states-server-containers-add-form :input[name=\u0022visible_textfield\u0022]&quot;:{&quot;filled&quot;:true}}}" />');
+    $this->assertRaw('<input data-drupal-selector="edit-visible-slide-textfield" type="text" id="edit-visible-slide-textfield" name="visible_slide_textfield" value="" size="60" maxlength="255" class="form-text" data-drupal-states="{&quot;required&quot;:{&quot;.webform-submission-test-states-server-containers-add-form :input[name=\u0022visible_trigger\u0022]&quot;:{&quot;checked&quot;:true}}}" />');
+    $this->assertRaw('<input data-drupal-selector="edit-visible-slide-custom-textfield" type="text" id="edit-visible-slide-custom-textfield" name="visible_slide_custom_textfield" value="" size="60" maxlength="255" class="form-text" data-drupal-states="{&quot;required&quot;:{&quot;.webform-submission-test-states-server-containers-add-form :input[name=\u0022visible_trigger\u0022]&quot;:{&quot;checked&quot;:true},&quot;.webform-submission-test-states-server-containers-add-form :input[name=\u0022visible_slide_textfield\u0022]&quot;:{&quot;filled&quot;:true}}}" />');
+    $this->assertRaw('<input data-drupal-selector="edit-visible-composite-items-0-textfield" type="text" id="edit-visible-composite-items-0-textfield" name="visible_composite[items][0][textfield]" value="" size="60" maxlength="255" class="form-text" data-drupal-states="{&quot;required&quot;:{&quot;.webform-submission-test-states-server-containers-add-form :input[name=\u0022visible_trigger\u0022]&quot;:{&quot;checked&quot;:true}}}" />');
 
     // Check nested element is required.
     $edit = [
@@ -380,6 +419,8 @@ public function testFormStatesValidatorRequired() {
     $this->assertNoRaw('visible_custom_textfield field is required.');
     $this->assertRaw('visible_slide_textfield field is required.');
     $this->assertNoRaw('visible_slide_custom_textfield field is required.');
+    $this->assertRaw('textfield field is required.');
+    $this->assertRaw('select_other field is required.');
 
     // Check nested element is not required.
     $edit = [];
@@ -388,6 +429,8 @@ public function testFormStatesValidatorRequired() {
     $this->assertNoRaw('visible_custom_textfield field is required.');
     $this->assertNoRaw('visible_slide_textfield field is required.');
     $this->assertNoRaw('visible_slide_custom_textfield field is required.');
+    $this->assertNoRaw('textfield field is required.');
+    $this->assertNoRaw('select_other field is required.');
 
     // Check custom states element validation.
     $edit = [
@@ -398,127 +441,57 @@ public function testFormStatesValidatorRequired() {
     $this->postSubmission($webform, $edit);
     $this->assertRaw('visible_custom_textfield field is required.');
     $this->assertRaw('visible_slide_custom_textfield field is required.');
-  }
-
-  /**
-   * Tests webform submission conditions (#states) validator wizard cross-page conditions.
-   */
-  public function testFormStatesValidatorWizard() {
-    $webform = Webform::load('test_form_states_server_wizard');
 
     /**************************************************************************/
-
-    // Go to default #states for page 02 with trigger-checkbox unchecked.
-    $this->postSubmission($webform, [], t('Next Page >'));
-
-    // Check trigger-checkbox value is No.
-    $this->assertRaw('<input data-drupal-selector="edit-page-01-trigger-checkbox-computed" type="hidden" name="page_01_trigger_checkbox_computed" value="No" />');
-
-    // Check page_02_textfield_required is not required.
-    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-required" aria-describedby="edit-page-02-textfield-required--description" type="text" id="edit-page-02-textfield-required" name="page_02_textfield_required" value="{default_value}" size="60" maxlength="255" class="form-text" />');
-
-    // Check page_02_textfield_optional is required.
-    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-optional" aria-describedby="edit-page-02-textfield-optional--description" type="text" id="edit-page-02-textfield-optional" name="page_02_textfield_optional" value="{default_value}" size="60" maxlength="255" class="form-text required" required="required" aria-required="true" />');
-
-    // Check page_02_textfield_disabled is not disabled.
-    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-disabled" aria-describedby="edit-page-02-textfield-disabled--description" type="text" id="edit-page-02-textfield-disabled" name="page_02_textfield_disabled" value="" size="60" maxlength="255" class="form-text" />');
-
-    // Check page_02_textfield_enabled is disabled.
-    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-enabled" aria-describedby="edit-page-02-textfield-enabled--description" disabled="disabled" type="text" id="edit-page-02-textfield-enabled" name="page_02_textfield_enabled" value="" size="60" maxlength="255" class="form-text" />');
-
-    // Check page_02_textfield_visible is not visible.
-    $this->assertNoFieldByName('page_02_textfield_visible');
-
-    // Check page_02_textfield_visible_slide is not visible.
-    $this->assertNoFieldByName('page_02_textfield_visible_slide');
-
-    // Check page_02_textfield_invisible is visible.
-    $this->assertFieldByName('page_02_textfield_invisible');
-
-    // Check page_02_textfield_invisible_slide is visible.
-    $this->assertFieldByName('page_02_textfield_invisible_slide');
-
-    // Check page_02_checkbox_checked is not checked.
-    $this->assertRaw('<input data-drupal-selector="edit-page-02-checkbox-checked" aria-describedby="edit-page-02-checkbox-checked--description" type="checkbox" id="edit-page-02-checkbox-checked" name="page_02_checkbox_checked" value="1" class="form-checkbox" />');
-
-    // Check page_02_checkbox_unchecked is checked.
-    $this->assertRaw('<input data-drupal-selector="edit-page-02-checkbox-unchecked" aria-describedby="edit-page-02-checkbox-unchecked--description" type="checkbox" id="edit-page-02-checkbox-unchecked" name="page_02_checkbox_unchecked" value="1" checked="checked" class="form-checkbox" />');
-
-    // Check page_02_details_expanded is not open.
-    $this->assertRaw('<details data-webform-details-nosave data-webform-key="page_02_details_expanded" data-drupal-selector="edit-page-02-details-expanded" aria-describedby="edit-page-02-details-expanded--description" id="edit-page-02-details-expanded" class="js-form-wrapper form-wrapper"> ');
-
-    // Check page_02_details_collapsed is open.
-    $this->assertRaw('<details data-webform-details-nosave data-webform-key="page_02_details_collapsed" data-drupal-selector="edit-page-02-details-collapsed" aria-describedby="edit-page-02-details-collapsed--description" id="edit-page-02-details-collapsed" class="js-form-wrapper form-wrapper" open="open">');
-
+    // nested conditions.
     /**************************************************************************/
 
-    // Go to default #states for page 02 with trigger_checkbox checked.
-    $this->postSubmission($webform, ['page_01_trigger_checkbox' => TRUE], t('Next Page >'));
-
-    // Check trigger-checkbox value is Yes.
-    $this->assertRaw('<input data-drupal-selector="edit-page-01-trigger-checkbox-computed" type="hidden" name="page_01_trigger_checkbox_computed" value="Yes" />');
-
-    // Check page_02_textfield_required is required.
-    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-required" aria-describedby="edit-page-02-textfield-required--description" type="text" id="edit-page-02-textfield-required" name="page_02_textfield_required" value="{default_value}" size="60" maxlength="255" class="form-text required" required="required" aria-required="true" />');
-
-    // Check page_02_textfield_optional is not required.
-    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-optional" aria-describedby="edit-page-02-textfield-optional--description" type="text" id="edit-page-02-textfield-optional" name="page_02_textfield_optional" value="{default_value}" size="60" maxlength="255" class="form-text" />');
-
-    // Check page_02_textfield_disabled is disabled.
-    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-disabled" aria-describedby="edit-page-02-textfield-disabled--description" disabled="disabled" type="text" id="edit-page-02-textfield-disabled" name="page_02_textfield_disabled" value="" size="60" maxlength="255" class="form-text" />');
+    $webform = Webform::load('test_states_server_nested');
 
-    // Check page_02_textfield_enabled is not disabled.
-    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-enabled" aria-describedby="edit-page-02-textfield-enabled--description" type="text" id="edit-page-02-textfield-enabled" name="page_02_textfield_enabled" value="" size="60" maxlength="255" class="form-text" />');
+    // Check a and b sets target required page 1.
+    $edit = ['a' => TRUE, 'b' => TRUE, 'c' => FALSE];
+    $this->drupalPostForm('webform/test_states_server_nested', $edit, t('Next Page >'));
+    $this->assertRaw('page_1_target: [a and b] or c = required field is required.');
 
-    // Check page_02_textfield_visible is visible.
-    $this->assertFieldByName('page_02_textfield_visible');
+    // Check c sets target required page 1.
+    $edit = ['a' => FALSE, 'b' => TRUE, 'c' => TRUE];
+    $this->drupalPostForm('webform/test_states_server_nested', $edit, t('Next Page >'));
+    $this->assertRaw('page_1_target: [a and b] or c = required field is required.');
 
-    // Check page_02_textfield_visible_slide is visible.
-    $this->assertFieldByName('page_02_textfield_visible_slide');
+    // Check none sets target not required page 1.
+    $edit = ['a' => FALSE, 'b' => FALSE, 'c' => FALSE];
+    $this->drupalPostForm('webform/test_states_server_nested', $edit, t('Next Page >'));
+    $this->assertNoRaw('page_1_target: [a and b] or c = required field is required.');
 
-    // Check page_02_textfield_invisible is not visible.
-    $this->assertNoFieldByName('page_02_textfield_invisible');
+    // Check none sets target not required page 2.
+    $this->assertRaw('<label for="edit-page-2-target">page_2_target: [a and b] or c = required</label>');
+    $this->assertRaw('<input data-drupal-selector="edit-page-2-target" type="text" id="edit-page-2-target" name="page_2_target" value="" size="60" maxlength="255" class="form-text" />');
 
-    // Check page_02_textfield_invisible_slide is not visible.
-    $this->assertNoFieldByName('page_02_textfield_invisible_slide');
+    // Check a and b sets target required page 2.
+    $edit = ['a' => TRUE, 'b' => TRUE, 'c' => FALSE, 'page_1_target' => '{value}'];
+    $this->drupalPostForm('webform/test_states_server_nested', $edit, t('Next Page >'));
+    $this->assertNoRaw('<input data-drupal-selector="edit-page-2-target" type="text" id="edit-page-2-target" name="page_2_target" value="" size="60" maxlength="255" class="form-text" />');
+    $this->assertRaw('<label for="edit-page-2-target" class="js-form-required form-required">page_2_target: [a and b] or c = required</label>');
+    $this->assertRaw('<input data-drupal-selector="edit-page-2-target" type="text" id="edit-page-2-target" name="page_2_target" value="" size="60" maxlength="255" class="form-text required" required="required" aria-required="true" />');
 
-    // Check page_02_checkbox_checked is checked.
-    $this->assertRaw('<input data-drupal-selector="edit-page-02-checkbox-checked" aria-describedby="edit-page-02-checkbox-checked--description" type="checkbox" id="edit-page-02-checkbox-checked" name="page_02_checkbox_checked" value="1" checked="checked" class="form-checkbox" />');
+    /**************************************************************************/
+    // test_states_crosspage.
+    /**************************************************************************/
 
-    // Check page_02_checkbox_unchecked is not checked.
-    $this->assertRaw('<input data-drupal-selector="edit-page-02-checkbox-unchecked" aria-describedby="edit-page-02-checkbox-unchecked--description" type="checkbox" id="edit-page-02-checkbox-unchecked" name="page_02_checkbox_unchecked" value="1" class="form-checkbox" />');
+    $webform = Webform::load('test_states_crosspage');
 
-    // Check page_02_details_expanded is open.
-    $this->assertRaw('<details data-webform-details-nosave data-webform-key="page_02_details_expanded" data-drupal-selector="edit-page-02-details-expanded" aria-describedby="edit-page-02-details-expanded--description" id="edit-page-02-details-expanded" class="js-form-wrapper form-wrapper" open="open">');
+    $trigger_1_name = 'webform_states_' . md5('.webform-submission-test-states-crosspage-add-form :input[name="trigger_1"]');
+    $trigger_2_name = 'webform_states_' . md5('.webform-submission-test-states-crosspage-add-form :input[name="trigger_2"]');
 
-    // Check page_02_details_collapsed is not open.
-    $this->assertRaw('<details data-webform-details-nosave data-webform-key="page_02_details_collapsed" data-drupal-selector="edit-page-02-details-collapsed" aria-describedby="edit-page-02-details-collapsed--description" id="edit-page-02-details-collapsed" class="js-form-wrapper form-wrapper">');
-  }
+    // Check cross page states attribute and input on page 1.
+    $this->drupalGet('/webform/test_states_crosspage');
+    $this->assertRaw(':input[name=\u0022' . $trigger_2_name . '\u0022]');
+    $this->assertFieldByName($trigger_2_name);
 
-  /**
-   * Tests conditions (#states) validator for elements .
-   */
-  public function testStatesValidatorElementVisible() {
-    $webform = Webform::load('test_form_states_server_preview');
-
-    // Check trigger unchecked and elements are conditionally hidden.
-    $this->postSubmission($webform, [], t('Preview'));
-    $this->assertRaw('trigger_checkbox');
-    $this->assertNoRaw('dependent_checkbox');
-    $this->assertNoRaw('dependent_markup');
-    $this->assertNoRaw('dependent_message');
-    $this->assertNoRaw('dependent_fieldset');
-    $this->assertNoRaw('nested_textfield');
-
-    // Check trigger checked and elements are conditionally visible.
-    $this->postSubmission($webform, ['trigger_checkbox' => TRUE], t('Preview'));
-    $this->assertRaw('trigger_checkbox');
-    $this->assertRaw('dependent_checkbox');
-    $this->assertRaw('dependent_markup');
-    $this->assertRaw('dependent_message');
-    $this->assertRaw('dependent_fieldset');
-    $this->assertRaw('nested_textfield');
+    // Check cross page states attribute and input on page 2.
+    $this->postSubmission($webform, ['trigger_1' => TRUE], t('Next Page >'));
+    $this->assertRaw(':input[name=\u0022' . $trigger_1_name . '\u0022]');
+    $this->assertFieldByName($trigger_1_name);
   }
 
 }
-
diff --git a/web/modules/webform/src/Tests/States/WebformStatesWizardTest.php b/web/modules/webform/src/Tests/States/WebformStatesWizardTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..9ae54351b00928e35b4efc7684923e92cefdf596
--- /dev/null
+++ b/web/modules/webform/src/Tests/States/WebformStatesWizardTest.php
@@ -0,0 +1,203 @@
+<?php
+
+namespace Drupal\webform\Tests\States;
+
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Tests\WebformTestBase;
+
+/**
+ * Tests for webform states wizard server.
+ *
+ * @group Webform
+ */
+class WebformStatesWizardTest extends WebformTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = [
+    'test_states_server_wizard',
+  ];
+
+  /**
+   * Tests webform submission conditions (#states) validator wizard cross-page conditions.
+   */
+  public function testFormStatesValidatorWizard() {
+    $webform = Webform::load('test_states_server_wizard');
+
+    /**************************************************************************/
+
+    // Go to default #states for page 02 with trigger-checkbox unchecked.
+    $this->postSubmission($webform, [], t('Next Page >'));
+
+    $this->assertRaw("page_01_trigger_checkbox: 0
+page_01_textfield_required: '{default_value}'
+page_01_textfield_optional: '{default_value}'
+page_01_textfield_disabled: ''
+page_01_textfield_enabled: ''
+page_01_textfield_visible: ''
+page_01_textfield_invisible: ''
+page_01_checkbox_checked: 0
+page_01_checkbox_unchecked: 0
+page_02_textfield_required: '{default_value}'
+page_02_textfield_optional: '{default_value}'
+page_02_textfield_disabled: ''
+page_02_textfield_enabled: ''
+page_02_textfield_visible: '{default_value}'
+page_02_textfield_visible_slide: '{default_value}'
+page_02_textfield_invisible: '{default_value}'
+page_02_textfield_invisible_slide: '{default_value}'
+page_02_checkbox_checked: 0
+page_02_checkbox_unchecked: 0");
+
+    // Check trigger-checkbox value is No.
+    $this->assertRaw('<input data-drupal-selector="edit-page-01-trigger-checkbox-computed" type="hidden" name="page_01_trigger_checkbox_computed" value="No" />');
+
+    // Check page_02_textfield_required is not required.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-required" aria-describedby="edit-page-02-textfield-required--description" type="text" id="edit-page-02-textfield-required" name="page_02_textfield_required" value="{default_value}" size="60" maxlength="255" class="form-text" />');
+
+    // Check page_02_textfield_optional is required.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-optional" aria-describedby="edit-page-02-textfield-optional--description" type="text" id="edit-page-02-textfield-optional" name="page_02_textfield_optional" value="{default_value}" size="60" maxlength="255" class="form-text required" required="required" aria-required="true" />');
+
+    // Check page_02_textfield_disabled is not disabled.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-disabled" aria-describedby="edit-page-02-textfield-disabled--description" type="text" id="edit-page-02-textfield-disabled" name="page_02_textfield_disabled" value="" size="60" maxlength="255" class="form-text" />');
+
+    // Check page_02_textfield_enabled is disabled.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-enabled" aria-describedby="edit-page-02-textfield-enabled--description" disabled="disabled" type="text" id="edit-page-02-textfield-enabled" name="page_02_textfield_enabled" value="" size="60" maxlength="255" class="form-text" />');
+
+    // Check page_02_textfield_visible is not visible.
+    $this->assertNoFieldByName('page_02_textfield_visible', '{default_value}');
+
+    // Check page_02_textfield_visible_slide is not visible.
+    $this->assertNoFieldByName('page_02_textfield_visible_slide', '{default_value}');
+
+    // Check page_02_textfield_invisible is visible.
+    $this->assertFieldByName('page_02_textfield_invisible', '{default_value}');
+
+    // Check page_02_textfield_invisible_slide is visible.
+    $this->assertFieldByName('page_02_textfield_invisible_slide', '{default_value}');
+
+    // Check page_02_checkbox_checked is not checked.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-checkbox-checked" aria-describedby="edit-page-02-checkbox-checked--description" type="checkbox" id="edit-page-02-checkbox-checked" name="page_02_checkbox_checked" value="1" class="form-checkbox" />');
+
+    // Check page_02_checkbox_unchecked is checked.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-checkbox-unchecked" aria-describedby="edit-page-02-checkbox-unchecked--description" type="checkbox" id="edit-page-02-checkbox-unchecked" name="page_02_checkbox_unchecked" value="1" checked="checked" class="form-checkbox" />');
+
+    // Check page_02_details_expanded is not open.
+    $this->assertRaw('<details data-webform-details-nosave data-webform-key="page_02_details_expanded" data-drupal-selector="edit-page-02-details-expanded" aria-describedby="edit-page-02-details-expanded--description" id="edit-page-02-details-expanded" class="js-form-wrapper form-wrapper"> ');
+
+    // Check page_02_details_collapsed is open.
+    $this->assertRaw('<details data-webform-details-nosave data-webform-key="page_02_details_collapsed" data-drupal-selector="edit-page-02-details-collapsed" aria-describedby="edit-page-02-details-collapsed--description" id="edit-page-02-details-collapsed" class="js-form-wrapper form-wrapper" open="open">');
+
+    // Check submission data.
+    $this->drupalPostForm(NULL, [], t('Submit'));
+    $this->assertRaw("page_01_trigger_checkbox: 0
+page_01_textfield_required: '{default_value}'
+page_01_textfield_optional: '{default_value}'
+page_01_textfield_disabled: ''
+page_01_textfield_enabled: ''
+page_01_textfield_visible: ''
+page_01_textfield_invisible: ''
+page_01_checkbox_checked: 0
+page_01_checkbox_unchecked: 0
+page_02_textfield_required: '{default_value}'
+page_02_textfield_optional: '{default_value}'
+page_02_textfield_disabled: ''
+page_02_textfield_enabled: ''
+page_02_textfield_visible: ''
+page_02_textfield_visible_slide: ''
+page_02_textfield_invisible: '{default_value}'
+page_02_textfield_invisible_slide: '{default_value}'
+page_02_checkbox_checked: 0
+page_02_checkbox_unchecked: 1");
+
+    /**************************************************************************/
+
+    // Go to default #states for page 02 with trigger_checkbox checked.
+    $this->postSubmission($webform, ['page_01_trigger_checkbox' => TRUE], t('Next Page >'));
+
+    $this->assertRaw("page_01_trigger_checkbox: 1
+page_01_textfield_required: '{default_value}'
+page_01_textfield_optional: '{default_value}'
+page_01_textfield_disabled: ''
+page_01_textfield_enabled: ''
+page_01_textfield_visible: ''
+page_01_textfield_invisible: ''
+page_01_checkbox_checked: 0
+page_01_checkbox_unchecked: 0
+page_02_textfield_required: '{default_value}'
+page_02_textfield_optional: '{default_value}'
+page_02_textfield_disabled: ''
+page_02_textfield_enabled: ''
+page_02_textfield_visible: '{default_value}'
+page_02_textfield_visible_slide: '{default_value}'
+page_02_textfield_invisible: '{default_value}'
+page_02_textfield_invisible_slide: '{default_value}'
+page_02_checkbox_checked: 0
+page_02_checkbox_unchecked: 0");
+
+    // Check trigger-checkbox value is Yes.
+    $this->assertRaw('<input data-drupal-selector="edit-page-01-trigger-checkbox-computed" type="hidden" name="page_01_trigger_checkbox_computed" value="Yes" />');
+
+    // Check page_02_textfield_required is required.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-required" aria-describedby="edit-page-02-textfield-required--description" type="text" id="edit-page-02-textfield-required" name="page_02_textfield_required" value="{default_value}" size="60" maxlength="255" class="form-text required" required="required" aria-required="true" />');
+
+    // Check page_02_textfield_optional is not required.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-optional" aria-describedby="edit-page-02-textfield-optional--description" type="text" id="edit-page-02-textfield-optional" name="page_02_textfield_optional" value="{default_value}" size="60" maxlength="255" class="form-text" />');
+
+    // Check page_02_textfield_disabled is disabled.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-disabled" aria-describedby="edit-page-02-textfield-disabled--description" disabled="disabled" type="text" id="edit-page-02-textfield-disabled" name="page_02_textfield_disabled" value="" size="60" maxlength="255" class="form-text" />');
+
+    // Check page_02_textfield_enabled is not disabled.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-textfield-enabled" aria-describedby="edit-page-02-textfield-enabled--description" type="text" id="edit-page-02-textfield-enabled" name="page_02_textfield_enabled" value="" size="60" maxlength="255" class="form-text" />');
+
+    // Check page_02_textfield_visible is visible.
+    $this->assertFieldByName('page_02_textfield_visible', '{default_value}');
+
+    // Check page_02_textfield_visible_slide is visible.
+    $this->assertFieldByName('page_02_textfield_visible_slide', '{default_value}');
+
+    // Check page_02_textfield_invisible is not visible.
+    $this->assertNoFieldByName('page_02_textfield_invisible', '{default_value}');
+
+    // Check page_02_textfield_invisible_slide is not visible.
+    $this->assertNoFieldByName('page_02_textfield_invisible_slide', '{default_value}');
+
+    // Check page_02_checkbox_checked is checked.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-checkbox-checked" aria-describedby="edit-page-02-checkbox-checked--description" type="checkbox" id="edit-page-02-checkbox-checked" name="page_02_checkbox_checked" value="1" checked="checked" class="form-checkbox" />');
+
+    // Check page_02_checkbox_unchecked is not checked.
+    $this->assertRaw('<input data-drupal-selector="edit-page-02-checkbox-unchecked" aria-describedby="edit-page-02-checkbox-unchecked--description" type="checkbox" id="edit-page-02-checkbox-unchecked" name="page_02_checkbox_unchecked" value="1" class="form-checkbox" />');
+
+    // Check page_02_details_expanded is open.
+    $this->assertRaw('<details data-webform-details-nosave data-webform-key="page_02_details_expanded" data-drupal-selector="edit-page-02-details-expanded" aria-describedby="edit-page-02-details-expanded--description" id="edit-page-02-details-expanded" class="js-form-wrapper form-wrapper" open="open">');
+
+    // Check page_02_details_collapsed is not open.
+    $this->assertRaw('<details data-webform-details-nosave data-webform-key="page_02_details_collapsed" data-drupal-selector="edit-page-02-details-collapsed" aria-describedby="edit-page-02-details-collapsed--description" id="edit-page-02-details-collapsed" class="js-form-wrapper form-wrapper">');
+
+    // Check submission data.
+    $this->drupalPostForm(NULL, [], t('Submit'));
+    $this->assertRaw("page_01_trigger_checkbox: 1
+page_01_textfield_required: '{default_value}'
+page_01_textfield_optional: '{default_value}'
+page_01_textfield_disabled: ''
+page_01_textfield_enabled: ''
+page_01_textfield_visible: ''
+page_01_textfield_invisible: ''
+page_01_checkbox_checked: 0
+page_01_checkbox_unchecked: 0
+page_02_textfield_required: '{default_value}'
+page_02_textfield_optional: '{default_value}'
+page_02_textfield_disabled: ''
+page_02_textfield_enabled: ''
+page_02_textfield_visible: '{default_value}'
+page_02_textfield_visible_slide: '{default_value}'
+page_02_textfield_invisible: ''
+page_02_textfield_invisible_slide: ''
+page_02_checkbox_checked: 1
+page_02_checkbox_unchecked: 0");
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Views/WebformViewsBulkFormTest.php b/web/modules/webform/src/Tests/Views/WebformViewsBulkFormTest.php
index e913c26a399e89b235624e18adc13b294d31c766..7da8a42fb0673dcec11e2eb0be33f87dbe515332 100644
--- a/web/modules/webform/src/Tests/Views/WebformViewsBulkFormTest.php
+++ b/web/modules/webform/src/Tests/Views/WebformViewsBulkFormTest.php
@@ -20,24 +20,20 @@ class WebformViewsBulkFormTest extends WebformTestBase {
    */
   public static $modules = ['webform', 'webform_test_views'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
   /**
    * Tests the webform views bulk form.
    */
   public function testViewsBulkForm() {
-    $this->drupalLogin($this->adminSubmissionUser);
+    $admin_submission_user = $this->drupalCreateUser([
+      'administer webform submission',
+    ]);
+
+    /**************************************************************************/
+
+    $this->drupalLogin($admin_submission_user);
 
     // Check no submissions.
-    $this->drupalGet('admin/structure/webform/test/views_bulk_form');
+    $this->drupalGet('/admin/structure/webform/test/views_bulk_form');
     $this->assertRaw('No submissions available.');
 
     // Create a test submission.
@@ -46,7 +42,7 @@ public function testViewsBulkForm() {
     $sid = $this->postSubmissionTest($webform);
     $webform_submission = $this->loadSubmission($sid);
 
-    $this->drupalLogin($this->adminSubmissionUser);
+    $this->drupalLogin($admin_submission_user);
 
     // Check make sticky action.
     $this->assertFalse($webform_submission->isSticky(), 'Webform submission is not sticky');
@@ -97,7 +93,7 @@ public function testViewsBulkForm() {
     $this->assertNull($webform_submission, '1: Webform submission has been deleted');
 
     // Check no submissions.
-    $this->drupalGet('admin/structure/webform/test/views_bulk_form');
+    $this->drupalGet('/admin/structure/webform/test/views_bulk_form');
     $this->assertRaw('No submissions available.');
   }
 
diff --git a/web/modules/webform/src/Tests/WebformAlterHooksTest.php b/web/modules/webform/src/Tests/WebformAlterHooksTest.php
index 457eadde0e78680ab4d44dfc3ff26adbc8d91740..1224b7010009aeeab01a57e5401b574b50caed83 100644
--- a/web/modules/webform/src/Tests/WebformAlterHooksTest.php
+++ b/web/modules/webform/src/Tests/WebformAlterHooksTest.php
@@ -23,7 +23,7 @@ class WebformAlterHooksTest extends WebformNodeTestBase {
    */
   public function testWebformAlterHooks() {
     // Check webform alter hooks.
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertRaw("hook_webform_submission_form_alter(): 'webform_submission_contact_add_form' executed.");
     $this->assertRaw("hook_form_alter(): 'webform_submission_contact_add_form' executed.");
     $this->assertRaw("hook_form_webform_submission_BASE_FORM_ID_form_alter(): 'webform_submission_contact_add_form' executed.");
diff --git a/web/modules/webform/src/Tests/WebformEditorTest.php b/web/modules/webform/src/Tests/WebformEditorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..95f40e4fe5c466b54f081bd13ca0ff20cfde7420
--- /dev/null
+++ b/web/modules/webform/src/Tests/WebformEditorTest.php
@@ -0,0 +1,258 @@
+<?php
+
+namespace Drupal\webform\Tests;
+
+use Drupal\file\Entity\File;
+use Drupal\webform\Entity\Webform;
+
+/**
+ * Tests for webform editor.
+ *
+ * @group Webform
+ */
+class WebformEditorTest extends WebformTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['file', 'webform'];
+
+  /**
+   * File usage manager.
+   *
+   * @var \Drupal\file\FileUsage\FileUsageInterface
+   */
+  protected $fileUsage;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->fileUsage = $this->container->get('file.usage');
+  }
+
+  /**
+   * Tests webform entity settings files.
+   */
+  public function testWebformSettingsFiles() {
+    $this->drupalLogin($this->rootUser);
+
+    // Create three test images.
+    /** @var \Drupal\file\FileInterface[] $images */
+    $images = $this->drupalGetTestFiles('image');
+    $images = array_slice($images, 0, 5);
+    foreach ($images as $index => $image_file) {
+      $images[$index] = File::create((array) $image_file);
+      $images[$index]->save();
+    }
+
+    // Check that all images are temporary.
+    $this->assertTrue($images[0]->isTemporary());
+    $this->assertTrue($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+
+    // Upload the first image.
+    $edit = [
+      'description[value]' => '<img data-entity-type="file" data-entity-uuid="' . $images[0]->uuid() . '"/>',
+    ];
+    $this->drupalPostForm('/admin/structure/webform/manage/contact/settings', $edit, t('Save'));
+    $this->reloadImages($images);
+
+    // Check that first image is not temporary.
+    $this->assertFalse($images[0]->isTemporary());
+    $this->assertTrue($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+
+    // Check create first image file usage.
+    $this->assertIdentical(['editor' => ['webform' => ['contact' => '1']]], $this->fileUsage->listUsage($images[0]), 'The file has 1 usage.');
+
+    // Upload the second image.
+    $edit = [
+      'description[value]' => '<img data-entity-type="file" data-entity-uuid="' . $images[0]->uuid() . '"/><img data-entity-type="file" data-entity-uuid="' . $images[1]->uuid() . '"/>',
+    ];
+    $this->drupalPostForm('/admin/structure/webform/manage/contact/settings', $edit, t('Save'));
+    $this->reloadImages($images);
+
+    // Check that first and second image are not temporary.
+    $this->assertFalse($images[0]->isTemporary());
+    $this->assertFalse($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+
+    // Check first and second image file usage.
+    $this->assertIdentical(['editor' => ['webform' => ['contact' => '1']]], $this->fileUsage->listUsage($images[0]), 'The file has 1 usage.');
+    $this->assertIdentical(['editor' => ['webform' => ['contact' => '1']]], $this->fileUsage->listUsage($images[1]), 'The file has 1 usage.');
+
+    // Remove the first image.
+    $edit = [
+      'description[value]' => '<img data-entity-type="file" data-entity-uuid="' . $images[1]->uuid() . '"/>',
+    ];
+    $this->drupalPostForm('/admin/structure/webform/manage/contact/settings', $edit, t('Save'));
+    $this->reloadImages($images);
+
+    // Check that first is temporary and second image is not temporary.
+    $this->assertTrue($images[0]->isTemporary());
+    $this->assertFalse($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+
+    // Check first and second image file usage.
+    $this->assertIdentical([], $this->fileUsage->listUsage($images[0]), 'The file has 0 usage.');
+    $this->assertIdentical(['editor' => ['webform' => ['contact' => '1']]], $this->fileUsage->listUsage($images[1]), 'The file has 1 usage.');
+
+    // Set all files back to temporary.
+    $edit = [
+      'description[value]' => '',
+    ];
+    $this->drupalPostForm('/admin/structure/webform/manage/contact/settings', $edit, t('Save'));
+    $this->reloadImages($images);
+
+    // Check that first and second image are temporary.
+    $this->assertTrue($images[0]->isTemporary());
+    $this->assertTrue($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+
+    // Stop marking unused files as temporary.
+    \Drupal::configFactory()->getEditable('webform.settings')
+      ->set('html_editor.make_unused_managed_files_temporary', FALSE)
+      ->save();
+    $this->assertTrue($images[0]->isTemporary());
+
+    // Check uploaded file is NOT temporary.
+    $this->assertTrue($images[0]->isTemporary());
+    $edit = [
+      'description[value]' => '<img data-entity-type="file" data-entity-uuid="' . $images[0]->uuid() . '"/>',
+    ];
+    $this->drupalPostForm('/admin/structure/webform/manage/contact/settings', $edit, t('Save'));
+    $this->reloadImages($images);
+    $this->assertFalse($images[0]->isTemporary());
+
+    // Check unused file is NOT temporary.
+    $edit = [
+      'description[value]' => '',
+    ];
+    $this->drupalPostForm('/admin/structure/webform/manage/contact/settings', $edit, t('Save'));
+    $this->reloadImages($images);
+    $this->assertFalse($images[0]->isTemporary());
+
+    // Start marking unused files as temporary.
+    \Drupal::configFactory()->getEditable('webform.settings')
+      ->set('html_editor.make_unused_managed_files_temporary', TRUE)
+      ->save();
+
+    $edit = [
+      'description[value]' => '<img data-entity-type="file" data-entity-uuid="' . $images[0]->uuid() . '"/>',
+    ];
+    $this->drupalPostForm('/admin/structure/webform/manage/contact/settings', $edit, t('Save'));
+    $this->reloadImages($images);
+
+    // Check that upload file is not temporary.
+    $this->assertFalse($images[0]->isTemporary());
+
+    // Delete the webform.
+    Webform::load('contact')->delete();
+    $this->reloadImages($images);
+
+    // Check that file is temporary after the webform is deleted.
+    $this->assertTrue($images[0]->isTemporary());
+  }
+
+  /**
+   * Tests webform configuration files.
+   */
+  public function testWebformConfigurationFiles() {
+    $this->drupalLogin($this->rootUser);
+
+    // Create three test images.
+    /** @var \Drupal\file\FileInterface[] $images */
+    $images = $this->drupalGetTestFiles('image');
+    $images = array_slice($images, 0, 5);
+    foreach ($images as $index => $image_file) {
+      $images[$index] = File::create((array) $image_file);
+      $images[$index]->save();
+    }
+
+    // Check that all images are temporary.
+    $this->assertTrue($images[0]->isTemporary());
+    $this->assertTrue($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+
+    // Upload the first image.
+    $edit = [
+      'form_settings[default_form_open_message][value]' => '<img data-entity-type="file" data-entity-uuid="' . $images[0]->uuid() . '"/>',
+    ];
+    $this->drupalPostForm('/admin/structure/webform/config', $edit, t('Save configuration'));
+    $this->reloadImages($images);
+
+    // Check that first image is not temporary.
+    $this->assertFalse($images[0]->isTemporary());
+    $this->assertTrue($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+
+    // Check create first image file usage.
+    $this->assertIdentical(['editor' => ['config' => ['webform.settings' => '1']]], $this->fileUsage->listUsage($images[0]), 'The file has 1 usage.');
+
+    // Upload the second image.
+    $edit = [
+      'form_settings[default_form_open_message][value]' => '<img data-entity-type="file" data-entity-uuid="' . $images[0]->uuid() . '"/><img data-entity-type="file" data-entity-uuid="' . $images[1]->uuid() . '"/>',
+    ];
+    $this->drupalPostForm('/admin/structure/webform/config', $edit, t('Save configuration'));
+    $this->reloadImages($images);
+
+    // Check that first and second image are not temporary.
+    $this->assertFalse($images[0]->isTemporary());
+    $this->assertFalse($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+
+    // Check first and second image file usage.
+    $this->assertIdentical(['editor' => ['config' => ['webform.settings' => '1']]], $this->fileUsage->listUsage($images[0]), 'The file has 1 usage.');
+    $this->assertIdentical(['editor' => ['config' => ['webform.settings' => '1']]], $this->fileUsage->listUsage($images[1]), 'The file has 1 usage.');
+
+    // Remove the first image.
+    $edit = [
+      'form_settings[default_form_open_message][value]' => '<img data-entity-type="file" data-entity-uuid="' . $images[1]->uuid() . '"/>',
+    ];
+    $this->drupalPostForm('/admin/structure/webform/config', $edit, t('Save configuration'));
+    $this->reloadImages($images);
+
+    // Check that first is temporary and second image is not temporary.
+    $this->assertTrue($images[0]->isTemporary());
+    $this->assertFalse($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+
+    // Check first and second image file usage.
+    $this->assertIdentical([], $this->fileUsage->listUsage($images[0]), 'The file has 0 usage.');
+    $this->assertIdentical(['editor' => ['config' => ['webform.settings' => '1']]], $this->fileUsage->listUsage($images[1]), 'The file has 1 usage.');
+
+    // Simulate deleting webform.settings.yml during webform uninstall.
+    // @see webform_uninstall()
+    $config = \Drupal::configFactory()->get('webform.settings');
+    _webform_config_delete($config);
+    $this->reloadImages($images);
+
+    // Check that first and second image are temporary.
+    $this->assertTrue($images[0]->isTemporary());
+    $this->assertTrue($images[1]->isTemporary());
+    $this->assertTrue($images[2]->isTemporary());
+  }
+
+  /****************************************************************************/
+  // Helper functions.
+  /****************************************************************************/
+
+  /**
+   * Reload images.
+   *
+   * @param array $images
+   *   An array of image files.
+   */
+  protected function reloadImages(array &$images) {
+    \Drupal::entityTypeManager()->getStorage('file')->resetCache();
+    foreach ($images as $index => $image) {
+      $images[$index] = File::load($image->id());
+    }
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/WebformEmailProviderTest.php b/web/modules/webform/src/Tests/WebformEmailProviderTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..40ace012b1492934ad8f6f9805dd3a7094c0fd77
--- /dev/null
+++ b/web/modules/webform/src/Tests/WebformEmailProviderTest.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\webform\Tests;
+
+/**
+ * Tests for webform email provider.
+ *
+ * @group Webform
+ */
+class WebformEmailProviderTest extends WebformTestBase {
+
+  /**
+   * Test webform email provider.
+   */
+  public function testEmailProvider() {
+    // Revert system.mail back to  php_mail.
+    $this->container->get('config.factory')
+      ->getEditable('system.mail')
+      ->set('interface.default', 'php_mail')
+      ->save();
+
+    /** @var \Drupal\webform\WebformEmailProviderInterface $email_provider */
+    $email_provider = \Drupal::service('webform.email_provider');
+
+    $this->drupalLogin($this->rootUser);
+
+    // Check Default PHP mailer is enabled because we manually changed the
+    // system.mail configuration.
+    $this->drupalGet('/admin/reports/status');
+    $this->assertRaw('Provided by php_mail mail plugin.');
+    $this->assertNoRaw("Webform PHP mailer: Sends the message as plain text or HTML, using PHP's native mail() function.");
+    $this->assertRaw('Default PHP mailer: Sends the message as plain text, using PHP\'s native mail() function.');
+
+    // Check Webform PHP mailer enabled after email provider check.
+    $email_provider->check();
+    $this->drupalGet('/admin/reports/status');
+    $this->assertRaw('Provided by the Webform module.');
+    $this->assertRaw("Webform PHP mailer: Sends the message as plain text or HTML, using PHP's native mail() function.");
+
+    // Check Mail System: Default PHP mailer after mailsystem module installed.
+    \Drupal::service('module_installer')->install(['mailsystem']);
+    $this->drupalGet('/admin/reports/status');
+    $this->assertRaw('Provided by the Mail System module.');
+    $this->assertNoRaw("Webform PHP mailer: Sends the message as plain text or HTML, using PHP's native mail() function.");
+    $this->assertRaw('Default PHP mailer: Sends the message as plain text, using PHP\'s native mail() function.');
+
+    // Check Webform PHP mailer enabled after mailsystem module uninstalled.
+    \Drupal::service('module_installer')->uninstall(['mailsystem']);
+    $this->drupalGet('/admin/reports/status');
+    $this->assertRaw("Webform PHP mailer: Sends the message as plain text or HTML, using PHP's native mail() function.");
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/WebformEntityTranslationTest.php b/web/modules/webform/src/Tests/WebformEntityTranslationTest.php
index 2bdae12fd76e7da6ee647125125741aa10c3f543..fef62cc8abd2c6f7ffafb38f1564fa49fe168cca 100644
--- a/web/modules/webform/src/Tests/WebformEntityTranslationTest.php
+++ b/web/modules/webform/src/Tests/WebformEntityTranslationTest.php
@@ -17,7 +17,7 @@ class WebformEntityTranslationTest extends WebformTestBase {
    *
    * @var array
    */
-  public static $modules = ['block', 'webform', 'webform_test_translation'];
+  public static $modules = ['block', 'webform', 'webform_ui', 'webform_test_translation'];
 
   /**
    * {@inheritdoc}
@@ -36,6 +36,10 @@ public function testTranslate() {
     // Login admin user.
     $this->drupalLogin($this->rootUser);
 
+    // Set [site:name] to 'Test Website' and translate it into Spanish.
+    $this->drupalPostForm('/admin/config/system/site-information', ['site_name' => 'Test Website'], t('Save configuration'));
+    $this->drupalPostForm('/admin/config/system/site-information/translate/es/add', ['translation[config_names][system.site][name]' => 'Sitio web de prueba'], t('Save translation'));
+
     /** @var \Drupal\webform\WebformTranslationManagerInterface $translation_manager */
     $translation_manager = \Drupal::service('webform.translation_manager');
 
@@ -44,43 +48,120 @@ public function testTranslate() {
     $elements = Yaml::decode($elements_raw);
 
     // Check translate tab.
-    $this->drupalGet('admin/structure/webform/manage/test_translation');
+    $this->drupalGet('/admin/structure/webform/manage/test_translation');
     $this->assertRaw('>Translate<');
 
     // Check translations.
-    $this->drupalGet('admin/structure/webform/manage/test_translation/translate');
+    $this->drupalGet('/admin/structure/webform/manage/test_translation/translate');
+    $this->assertRaw('<a href="' . base_path() . 'webform/test_translation"><strong>English (original)</strong></a>');
+    $this->assertRaw('<a href="' . base_path() . 'es/webform/test_translation" hreflang="es">Spanish</a>');
+    $this->assertNoRaw('<a href="' . base_path() . 'fr/webform/test_translation" hreflang="fr">French</a>');
     $this->assertRaw('<a href="' . base_path() . 'admin/structure/webform/manage/test_translation/translate/es/edit">Edit</a>');
 
-    // Check Spanish translations.
-    $this->drupalGet('admin/structure/webform/manage/test_translation/translate/es/edit');
+    // Check Spanish translation.
+    $this->drupalGet('/admin/structure/webform/manage/test_translation/translate/es/edit');
     $this->assertFieldByName('translation[config_names][webform.webform.test_translation][title]', 'Prueba: Traducción');
     $this->assertField('translation[config_names][webform.webform.test_translation][elements]');
 
+    // Check form builder is not translated.
+    $this->drupalGet('/es/admin/structure/webform/manage/test_translation');
+    $this->assertLink('Text field');
+    $this->assertNoLink('Campo de texto');
+
+    // Check form builder is not translated when reset.
+    $this->drupalPostAjaxForm('es/admin/structure/webform/manage/test_translation', [], ['op' => t('Reset')]);
+    $this->assertLink('Text field');
+    $this->assertNoLink('Campo de texto');
+
+    // Check element edit form is not translated.
+    $this->drupalGet('/es/admin/structure/webform/manage/test_translation/element/textfield/edit');
+    $this->assertFieldByName('properties[title]', 'Text field');
+    $this->assertNoFieldByName('properties[title]', 'Campo de texto');
+
     // Check translated webform options.
-    $this->drupalGet('es/webform/test_translation');
+    $this->drupalGet('/es/webform/test_translation');
     $this->assertRaw('<label for="edit-textfield">Campo de texto</label>');
     $this->assertRaw('<option value="1">Uno</option>');
     $this->assertRaw('<option value="4">Las cuatro</option>');
 
     // Check translated webform custom composite.
-    $this->drupalGet('es/webform/test_translation');
-    $this->assertRaw('<label for="edit-composite">Compuesto</label>');
+    $this->drupalGet('/es/webform/test_translation');
+    $this->assertRaw('<label>Compuesto</label>');
     $this->assertRaw('<th class="composite-table--first_name webform-multiple-table--first_name">Nombre</th>');
     $this->assertRaw('<th class="composite-table--last_name webform-multiple-table--last_name">Apellido</th>');
     $this->assertRaw('<th class="composite-table--age webform-multiple-table--age">Edad</th>');
     $this->assertRaw('<span class="field-suffix">años. antiguo</span>');
 
+    // Check translated webform address.
+    $this->drupalGet('/es/webform/test_translation');
+    $this->assertRaw('<span class="visually-hidden fieldset-legend">Dirección</span>');
+    $this->assertRaw('<label for="edit-address-address">Dirección</label>');
+    $this->assertRaw('<label for="edit-address-address-2">Dirección 2</label>');
+    $this->assertRaw('<label for="edit-address-city">Ciudad / Pueblo</label>');
+    $this->assertRaw('<label for="edit-address-state-province">Estado / Provincia</label>');
+    $this->assertRaw('<label for="edit-address-postal-code">ZIP / Código Postal</label>');
+    $this->assertRaw('<label for="edit-address-country">Acciones de país</label>');
+
+    // Check translated webform token.
+    $this->assertRaw('Site name: Sitio web de prueba');
+
     // Check that webform is not translated into French.
-    $this->drupalGet('fr/webform/test_translation');
+    $this->drupalGet('/fr/webform/test_translation');
     $this->assertRaw('<label for="edit-textfield">Text field</label>');
     $this->assertRaw('<option value="1">One</option>');
     $this->assertRaw('<option value="4">Four</option>');
+    $this->assertRaw('Site name: Test Website');
 
     // Check that French config elements returns the default languages elements.
     // Please note: This behavior might change.
     $translation_element = $translation_manager->getElements($webform, 'fr', TRUE);
     $this->assertEqual($elements, $translation_element);
 
+    // Translate [site:name] into French.
+    $this->drupalPostForm('/admin/config/system/site-information/translate/fr/add', ['translation[config_names][system.site][name]' => 'Site Web de test'], t('Save translation'));
+
+    // Check default elements.
+    $this->drupalGet('/admin/structure/webform/manage/test_translation/translate/fr/add');
+    $this->assertRaw('<textarea lang="fr" data-drupal-selector="edit-translation-config-names-webformwebformtest-translation-elements" aria-describedby="edit-translation-config-names-webformwebformtest-translation-elements--description" class="js-webform-codemirror webform-codemirror yaml form-textarea resize-vertical" data-webform-codemirror-mode="text/x-yaml" id="edit-translation-config-names-webformwebformtest-translation-elements" name="translation[config_names][webform.webform.test_translation][elements]" rows="48" cols="60">textfield:
+  &#039;#title&#039;: &#039;Text field&#039;
+select_options:
+  &#039;#title&#039;: &#039;Select (options)&#039;
+select_custom:
+  &#039;#title&#039;: &#039;Select (custom)&#039;
+  &#039;#options&#039;:
+    4: Four
+    5: Five
+    6: Six
+  &#039;#other__option_label&#039;: &#039;Custom number…&#039;
+details:
+  &#039;#title&#039;: Details
+markup:
+  &#039;#markup&#039;: &#039;This is some HTML markup.&#039;
+composite:
+  &#039;#title&#039;: Composite
+  &#039;#element&#039;:
+    first_name:
+      &#039;#title&#039;: &#039;First name&#039;
+    last_name:
+      &#039;#title&#039;: &#039;Last name&#039;
+    age:
+      &#039;#title&#039;: Age
+      &#039;#field_suffix&#039;: &#039; yrs. old&#039;
+address:
+  &#039;#title&#039;: Address
+  &#039;#address__title&#039;: Address
+  &#039;#address_2__title&#039;: &#039;Address 2&#039;
+  &#039;#city__title&#039;: City/Town
+  &#039;#state_province__title&#039;: State/Province
+  &#039;#postal_code__title&#039;: &#039;ZIP/Postal Code&#039;
+  &#039;#country__title&#039;: Country
+token:
+  &#039;#title&#039;: &#039;Computed (token)&#039;
+actions:
+  &#039;#title&#039;: &#039;Submit button(s)&#039;
+  &#039;#submit__label&#039;: &#039;Send message&#039;</textarea>
+</div>');
+
     // Create French translation.
     $translation_elements = [
       'textfield' => [
@@ -97,8 +178,15 @@ public function testTranslate() {
     $this->drupalPostForm('admin/structure/webform/manage/test_translation/translate/fr/add', $edit, t('Save translation'));
 
     // Check French translation.
-    $this->drupalGet('fr/webform/test_translation');
+    $this->drupalGet('/fr/webform/test_translation');
     $this->assertRaw('<label for="edit-textfield">French</label>');
+    $this->assertRaw('Site name: Site Web de test');
+
+    // Check translations.
+    $this->drupalGet('/admin/structure/webform/manage/test_translation/translate');
+    $this->assertRaw('<a href="' . base_path() . 'webform/test_translation"><strong>English (original)</strong></a>');
+    $this->assertRaw('<a href="' . base_path() . 'es/webform/test_translation" hreflang="es">Spanish</a>');
+    $this->assertRaw('<a href="' . base_path() . 'fr/webform/test_translation" hreflang="fr">French</a>');
 
     // Check French config elements only contains translated properties and
     // custom properties are removed.
@@ -110,21 +198,33 @@ public function testTranslate() {
     /**************************************************************************/
 
     // Check English table headers are not translated.
-    $this->drupalGet('admin/structure/webform/manage/test_translation/results/submissions');
+    $this->drupalGet('/admin/structure/webform/manage/test_translation/results/submissions');
     $this->assertRaw('>Text field<');
     $this->assertRaw('>Select (options)<');
     $this->assertRaw('>Select (custom)<');
     $this->assertRaw('>Composite<');
 
     // Check Spanish table headers are translated.
-    $this->drupalGet('es/admin/structure/webform/manage/test_translation/results/submissions');
+    $this->drupalGet('/es/admin/structure/webform/manage/test_translation/results/submissions');
     $this->assertRaw('>Campo de texto<');
     $this->assertRaw('>Seleccione (opciones)<');
     $this->assertRaw('>Seleccione (personalizado)<');
     $this->assertRaw('>Compuesto<');
 
+    // Create translated submissions.
+    $this->drupalPostForm('webform/test_translation', ['textfield' => 'English Submission'], 'Send message');
+    $this->drupalPostForm('es/webform/test_translation', ['textfield' => 'Spanish Submission'], 'Enviar mensaje');
+    $this->drupalPostForm('fr/webform/test_translation', ['textfield' => 'French Submission'], 'Send message');
+
+    // Check computed token is NOT translated for each language because only
+    // one language can be loaded for a config translation.
+    $this->drupalGet('/admin/structure/webform/manage/test_translation/results/submissions');
+    $this->assertRaw('Site name: Test Website');
+    $this->assertNoRaw('Site name: Sitio web de prueba');
+    $this->assertNoRaw('Site name: Sitio web de prueba');
+
     /**************************************************************************/
-    // Site wide language
+    // Site wide language.
     /**************************************************************************/
 
     // Make sure the site language is English (en).
@@ -132,30 +232,30 @@ public function testTranslate() {
 
     $language_manager = \Drupal::languageManager();
 
-    $this->drupalGet('webform/test_translation', ['language' => $language_manager->getLanguage('en')]);
+    $this->drupalGet('/webform/test_translation', ['language' => $language_manager->getLanguage('en')]);
     $this->assertRaw('<label for="edit-textfield">Text field</label>');
 
     // Check Spanish translation.
-    $this->drupalGet('webform/test_translation', ['language' => $language_manager->getLanguage('es')]);
+    $this->drupalGet('/webform/test_translation', ['language' => $language_manager->getLanguage('es')]);
     $this->assertRaw('<label for="edit-textfield">Campo de texto</label>');
 
     // Check French translation.
-    $this->drupalGet('webform/test_translation', ['language' => $language_manager->getLanguage('fr')]);
+    $this->drupalGet('/webform/test_translation', ['language' => $language_manager->getLanguage('fr')]);
     $this->assertRaw('<label for="edit-textfield">French</label>');
 
     // Change site language to French (fr).
     \Drupal::configFactory()->getEditable('system.site')->set('default_langcode', 'fr')->save();
 
     // Check English translation.
-    $this->drupalGet('webform/test_translation', ['language' => $language_manager->getLanguage('en')]);
+    $this->drupalGet('/webform/test_translation', ['language' => $language_manager->getLanguage('en')]);
     $this->assertRaw('<label for="edit-textfield">Text field</label>');
 
     // Check Spanish translation.
-    $this->drupalGet('webform/test_translation', ['language' => $language_manager->getLanguage('es')]);
+    $this->drupalGet('/webform/test_translation', ['language' => $language_manager->getLanguage('es')]);
     $this->assertRaw('<label for="edit-textfield">Campo de texto</label>');
 
     // Check French translation.
-    $this->drupalGet('webform/test_translation', ['language' => $language_manager->getLanguage('fr')]);
+    $this->drupalGet('/webform/test_translation', ['language' => $language_manager->getLanguage('fr')]);
     $this->assertRaw('<label for="edit-textfield">French</label>');
 
     /**************************************************************************/
@@ -170,16 +270,16 @@ public function testTranslate() {
     ];
     $this->drupalPostForm('admin/structure/webform/manage/test_translation/duplicate', $edit, t('Save'));
 
-    // Check duplicate  English translation.
-    $this->drupalGet('webform/duplicate', ['language' => $language_manager->getLanguage('en')]);
+    // Check duplicate English translation.
+    $this->drupalGet('/webform/duplicate', ['language' => $language_manager->getLanguage('en')]);
     $this->assertRaw('<label for="edit-textfield">Text field</label>');
 
     // Check duplicate Spanish translation.
-    $this->drupalGet('webform/duplicate', ['language' => $language_manager->getLanguage('es')]);
+    $this->drupalGet('/webform/duplicate', ['language' => $language_manager->getLanguage('es')]);
     $this->assertRaw('<label for="edit-textfield">Campo de texto</label>');
 
     // Check duplicate French translation.
-    $this->drupalGet('webform/duplicate', ['language' => $language_manager->getLanguage('fr')]);
+    $this->drupalGet('/webform/duplicate', ['language' => $language_manager->getLanguage('fr')]);
     $this->assertRaw('<label for="edit-textfield">French</label>');
   }
 
diff --git a/web/modules/webform/src/Tests/WebformHelpTest.php b/web/modules/webform/src/Tests/WebformHelpTest.php
index 05d08df2c2d8e5139d537535f172cccbf1f8531f..7dc184f3554318c469cd1cc8f304ae2f525d56c2 100644
--- a/web/modules/webform/src/Tests/WebformHelpTest.php
+++ b/web/modules/webform/src/Tests/WebformHelpTest.php
@@ -32,28 +32,42 @@ public function testHelp() {
     $this->drupalLogin($this->rootUser);
 
     // Check notifications, promotion, and welcome messages displayed.
-    $this->drupalGet('admin/structure/webform');
+    $this->drupalGet('/admin/structure/webform');
     $this->assertRaw('This is a warning notification.');
     $this->assertRaw('This is an info notification.');
     $this->assertRaw('The Drupal Association brings value to Drupal and to you.');
     $this->assertRaw('Welcome to the Webform module for Drupal 8.');
 
     // Close all notifications, promotion, and welcome messages.
-    $this->drupalGet('admin/structure/webform');
+    $this->drupalGet('/admin/structure/webform');
     $this->clickLink('×', 0);
-    $this->drupalGet('admin/structure/webform');
+    $this->drupalGet('/admin/structure/webform');
     $this->clickLink('×', 0);
-    $this->drupalGet('admin/structure/webform');
+    $this->drupalGet('/admin/structure/webform');
     $this->clickLink('×', 0);
-    $this->drupalGet('admin/structure/webform');
+    $this->drupalGet('/admin/structure/webform');
     $this->clickLink('×', 0);
 
     // Check notifications, promotion, and welcome messages closed.
-    $this->drupalGet('admin/structure/webform');
+    $this->drupalGet('/admin/structure/webform');
     $this->assertNoRaw('This is a warning notification.');
     $this->assertNoRaw('This is an info notification.');
     $this->assertNoRaw('The Drupal Association brings value to Drupal and to you.');
     $this->assertNoRaw('Welcome to the Webform module for Drupal 8.');
+
+    // Check that help is enabled.
+    $this->drupalGet('/admin/structure/webform/config/advanced');
+    $this->assertRaw('block block-help block-help-block');
+    $this->assertRaw('The <strong>Advanced configuration</strong> page allows an administrator to enable/disable UI behaviors, manage requirements and define data used for testing webforms.');
+
+    // Disable help via the UI which will clear the cached help block.
+    $this->drupalPostForm('admin/structure/webform/config/advanced', ['ui[help_disabled]' => TRUE], t('Save configuration'));
+
+    // Check that help is disabled.
+    $this->drupalGet('/admin/structure/webform/config/advanced');
+    $this->assertNoRaw('block block-help block-help-block');
+    $this->assertNoRaw('The <strong>Advanced configuration</strong> page allows an administrator to enable/disable UI behaviors, manage requirements and define data used for testing webforms.');
+
   }
 
 }
diff --git a/web/modules/webform/src/Tests/WebformLibrariesTest.php b/web/modules/webform/src/Tests/WebformLibrariesTest.php
index d8a2e7c4ff0d672622cae988aed03afec002a7ac..90c7b354df194133dfa30feb9c9fd7b6f968f708 100644
--- a/web/modules/webform/src/Tests/WebformLibrariesTest.php
+++ b/web/modules/webform/src/Tests/WebformLibrariesTest.php
@@ -23,16 +23,6 @@ class WebformLibrariesTest extends WebformTestBase {
    */
   protected static $testWebforms = ['test_libraries_optional'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
   /**
    * Tests webform libraries.
    */
@@ -50,16 +40,16 @@ public function testLibraries() {
 
     // Enable jquery.chosen and jquery.icheck.
     $edit = [
-      'libraries[excluded_libraries][jquery.chosen]' => TRUE,
-      'libraries[excluded_libraries][jquery.icheck]' => TRUE,
+      'excluded_libraries[jquery.chosen]' => TRUE,
+      'excluded_libraries[jquery.icheck]' => TRUE,
     ];
     $this->drupalPostForm('admin/structure/webform/config/libraries', $edit, t('Save configuration'));
 
     // Check optional libraries are included.
-    $this->drupalGet('webform/test_libraries_optional');
+    $this->drupalGet('/webform/test_libraries_optional');
     $this->assertRaw('/select2.min.js');
-    $this->assertRaw('/chosen.jquery.js');
-    $this->assertRaw('/jquery.word-and-character-counter.min.js');
+    $this->assertRaw('/chosen.jquery.min.js');
+    $this->assertRaw('/textcounter.min.js');
     $this->assertRaw('/intlTelInput.min.js');
     $this->assertRaw('/jquery.inputmask.bundle.min.js');
     $this->assertRaw('/icheck.js');
@@ -74,25 +64,25 @@ public function testLibraries() {
 
     // Exclude optional libraries.
     $edit = [
-      'libraries[excluded_libraries][ckeditor.fakeobjects]' => FALSE,
-      'libraries[excluded_libraries][ckeditor.image]' => FALSE,
-      'libraries[excluded_libraries][ckeditor.link]' => FALSE,
-      'libraries[excluded_libraries][codemirror]' => FALSE,
-      'libraries[excluded_libraries][jquery.icheck]' => FALSE,
-      'libraries[excluded_libraries][jquery.inputmask]' => FALSE,
-      'libraries[excluded_libraries][jquery.intl-tel-input]' => FALSE,
-      'libraries[excluded_libraries][jquery.select2]' => FALSE,
-      'libraries[excluded_libraries][jquery.chosen]' => FALSE,
-      'libraries[excluded_libraries][jquery.timepicker]' => FALSE,
-      'libraries[excluded_libraries][jquery.word-and-character-counter]' => FALSE,
+      'excluded_libraries[ckeditor.fakeobjects]' => FALSE,
+      'excluded_libraries[ckeditor.image]' => FALSE,
+      'excluded_libraries[ckeditor.link]' => FALSE,
+      'excluded_libraries[codemirror]' => FALSE,
+      'excluded_libraries[jquery.icheck]' => FALSE,
+      'excluded_libraries[jquery.inputmask]' => FALSE,
+      'excluded_libraries[jquery.intl-tel-input]' => FALSE,
+      'excluded_libraries[jquery.select2]' => FALSE,
+      'excluded_libraries[jquery.chosen]' => FALSE,
+      'excluded_libraries[jquery.timepicker]' => FALSE,
+      'excluded_libraries[jquery.textcounter]' => FALSE,
     ];
     $this->drupalPostForm('admin/structure/webform/config/libraries', $edit, t('Save configuration'));
 
     // Check optional libraries are excluded.
-    $this->drupalGet('webform/test_libraries_optional');
+    $this->drupalGet('/webform/test_libraries_optional');
     $this->assertNoRaw('/select2.min.js');
-    $this->assertNoRaw('/chosen.jquery.js');
-    $this->assertNoRaw('/jquery.word-and-character-counter.min.js');
+    $this->assertNoRaw('/chosen.jquery.min.js');
+    $this->assertNoRaw('/textcounter.min.js');
     $this->assertNoRaw('/intlTelInput.min.js');
     $this->assertNoRaw('/jquery.inputmask.bundle.min.js');
     $this->assertNoRaw('/icheck.js');
@@ -106,25 +96,24 @@ public function testLibraries() {
     }
 
     // Check that status report excludes optional libraries.
-    $this->drupalGet('admin/reports/status');
-    $this->assertText('The CKEditor: Fakeobjects library is excluded.');
-    $this->assertText('The CKEditor: Image library is excluded.');
-    $this->assertText('The CKEditor: Link library is excluded.');
-    $this->assertText('The Code Mirror library is excluded.');
-    $this->assertText('The jQuery: iCheck library is excluded.');
-    $this->assertText('The jQuery: Input Mask library is excluded.');
-    $this->assertText('The jQuery: Select2 library is excluded.');
-    $this->assertText('The jQuery: Chosen library is excluded.');
-    $this->assertText('The jQuery: Timepicker library is excluded.');
-    $this->assertText('The jQuery: Word and character counter plug-in! library is excluded.');
+    $this->drupalGet('/admin/reports/status');
+    $this->assertNoText('CKEditor: Fakeobjects library ');
+    $this->assertNoText('CKEditor: Image library ');
+    $this->assertNoText('CKEditor: Link library ');
+    $this->assertNoText('Code Mirror library ');
+    $this->assertNoText('jQuery: iCheck library ');
+    $this->assertNoText('jQuery: Input Mask library ');
+    $this->assertNoText('jQuery: Select2 library ');
+    $this->assertNoText('jQuery: Chosen library ');
+    $this->assertNoText('jQuery: Timepicker library ');
+    $this->assertNoText('jQuery: Text Counter library ');
 
     // Issue #2934542: Fix broken Webform.Drupal\webform\Tests\WebformLibrariesTest
     // @see https://www.drupal.org/project/webform/issues/2934542
     /*
     // Exclude element types that require libraries.
     $edit = [
-      'excluded_elements[webform_image_select]' => FALSE,
-      'excluded_elements[webform_location]' => FALSE,
+      'excluded_elements[webform_location_geocomplete]' => FALSE,
       'excluded_elements[webform_rating]' => FALSE,
       'excluded_elements[webform_signature]' => FALSE,
       'excluded_elements[webform_toggle]' => FALSE,
@@ -133,13 +122,34 @@ public function testLibraries() {
     $this->drupalPostForm('admin/structure/webform/config/elements', $edit, t('Save configuration'));
 
     // Check that status report excludes libraries required by element types.
-    $this->drupalGet('admin/reports/status');
-    $this->assertText('The jQuery: Geocoding and Places Autocomplete Plugin library is excluded because required element types (webform_location) are excluded.');
-    $this->assertText('The jQuery: Image Picker library is excluded because required element types (webform_image_select) are excluded.');
-    $this->assertText('The jQuery: RateIt library is excluded because required element types (webform_rating) are excluded.');
-    $this->assertText('The jQuery: Toggles library is excluded because required element types (webform_toggle; webform_toggles) are excluded.');
-    $this->assertText('The Signature Pad library is excluded because required element types (webform_signature) are excluded.');
+    $this->drupalGet('/admin/reports/status');
+    $this->assertNoText('jQuery: Geocoding and Places Autocomplete Plugin library');
+    $this->assertNoText('jQuery: Image Picker library');
+    $this->assertNoText('jQuery: RateIt library');
+    $this->assertNoText('jQuery: Toggles library');
+    $this->assertNoText('Signature Pad library');
     */
+
+    // Check that chosen and select2 using webform's CDN URLs.
+    $edit = [
+      'excluded_libraries[jquery.select2]' => TRUE,
+      'excluded_libraries[jquery.chosen]' => TRUE,
+    ];
+    $this->drupalPostForm('admin/structure/webform/config/libraries', $edit, t('Save configuration'));
+    $this->drupalGet('/webform/test_libraries_optional');
+    $this->assertRaw('https://cdnjs.cloudflare.com/ajax/libs/chosen');
+    $this->assertRaw('https://cdnjs.cloudflare.com/ajax/libs/select2');
+
+    // Install chosen and select2 modules.
+    \Drupal::service('module_installer')->install(['chosen', 'chosen_lib', 'select2']);
+    drupal_flush_all_caches();
+
+    // Check that chosen and select2 using module's path and not CDN.
+    $this->drupalGet('/webform/test_libraries_optional');
+    $this->assertNoRaw('https://cdnjs.cloudflare.com/ajax/libs/chosen');
+    $this->assertNoRaw('https://cdnjs.cloudflare.com/ajax/libs/select2');
+    $this->assertRaw('/modules/contrib/chosen/css/chosen-drupal.css');
+    $this->assertRaw('/libraries/select2/dist/css/select2.min.css');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/WebformOptionsTest.php b/web/modules/webform/src/Tests/WebformOptionsTest.php
index fc319d18f9549fc80ee4f472091038b80d6a38e2..b113381a3e412b8243e853a894e59e623075ebf2 100644
--- a/web/modules/webform/src/Tests/WebformOptionsTest.php
+++ b/web/modules/webform/src/Tests/WebformOptionsTest.php
@@ -26,21 +26,23 @@ class WebformOptionsTest extends WebformTestBase {
    */
   protected static $testWebforms = ['test_options'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
   /**
    * Tests webform options entity.
    */
   public function testWebformOptions() {
-    $this->drupalLogin($this->normalUser);
+    $normal_user = $this->drupalCreateUser();
+
+    $admin_user = $this->drupalCreateUser([
+      'access site reports',
+      'administer site configuration',
+      'administer webform',
+      'create webform',
+      'administer users',
+    ]);
+
+    /**************************************************************************/
+
+    $this->drupalLogin($normal_user);
 
     // Check get element options.
     $yes_no_options = ['Yes' => 'Yes', 'No' => 'No'];
@@ -82,7 +84,7 @@ public function testWebformOptions() {
 
     // Check hook_webform_options_alter() && hook_webform_options_WEBFORM_OPTIONS_ID_alter().
     // Check that the default value can be set from the alter hook.
-    $this->drupalGet('webform/test_options');
+    $this->drupalGet('/webform/test_options');
     $this->assertRaw('<select data-drupal-selector="edit-custom" id="edit-custom" name="custom" class="form-select"><option value="">- None -</option><option value="one" selected="selected">One</option><option value="two">Two</option><option value="three">Three</option></select>');
     $this->assertRaw('<select data-drupal-selector="edit-test" id="edit-test" name="test" class="form-select"><option value="" selected="selected">- None -</option><option value="four">Four</option><option value="five">Five</option><option value="six">Six</option></select>');
 
@@ -93,11 +95,11 @@ public function testWebformOptions() {
     $webform_test_options->save();
     $this->debug($webform_test_options->getOptions());
 
-    $this->drupalGet('webform/test_options');
+    $this->drupalGet('/webform/test_options');
     $this->assertRaw('<select data-drupal-selector="edit-test" id="edit-test" name="test" class="form-select"><option value="" selected="selected">- None -</option><option value="red">Red</option><option value="white">White</option><option value="blue">Blue</option><option value="four">Four</option><option value="five">Five</option><option value="six">Six</option></select>');
 
     // Check custom options set via alter hook().
-    $this->drupalGet('webform/test_options');
+    $this->drupalGet('/webform/test_options');
     $this->assertRaw('<select data-drupal-selector="edit-test" id="edit-test" name="test" class="form-select"><option value="" selected="selected">- None -</option><option value="red">Red</option><option value="white">White</option><option value="blue">Blue</option><option value="four">Four</option><option value="five">Five</option><option value="six">Six</option></select>');
 
     // Check that 'Afghanistan' is the first option.
@@ -116,16 +118,16 @@ public function testWebformOptions() {
     $this->assertEqual(reset($options), 'Switzerland');
 
     // Check admin user access denied.
-    $this->drupalGet('admin/structure/webform/config/options/manage');
+    $this->drupalGet('/admin/structure/webform/config/options/manage');
     $this->assertResponse(403);
-    $this->drupalGet('admin/structure/webform/config/options/manage/add');
+    $this->drupalGet('/admin/structure/webform/config/options/manage/add');
     $this->assertResponse(403);
 
     // Check admin user access.
-    $this->drupalLogin($this->adminWebformUser);
-    $this->drupalGet('admin/structure/webform/config/options/manage');
+    $this->drupalLogin($admin_user);
+    $this->drupalGet('/admin/structure/webform/config/options/manage');
     $this->assertResponse(200);
-    $this->drupalGet('admin/structure/webform/config/options/manage/add');
+    $this->drupalGet('/admin/structure/webform/config/options/manage/add');
     $this->assertResponse(200);
   }
 
diff --git a/web/modules/webform/src/Tests/WebformRenderingTest.php b/web/modules/webform/src/Tests/WebformRenderingTest.php
index edbd547059a7d657f74bc742f3ca3c50e0d91c92..94b75dde0edc3f6ac4dca40af4c404d5dd92df1a 100644
--- a/web/modules/webform/src/Tests/WebformRenderingTest.php
+++ b/web/modules/webform/src/Tests/WebformRenderingTest.php
@@ -88,7 +88,7 @@ public function testRendering() {
     // $this->assertContains($html_email['params']['body'], '<b><em>textfield_markup</em></b><br /><em>{prefix}</em>{default_value}<em>{suffix}</em><br /><br />');
     // $this->assertContains($html_email['params']['body'], '<b>textfield_special_characters (&amp;&gt;&lt;#)</b><br />(&amp;&gt;&lt;#){default_value}(&amp;&gt;&lt;#)<br /><br />');
     // $this->assertContains($html_email['params']['body'], '<b>text_format_basic_html</b><br /><p><em>{default_value}</em></p><br /><br />');
-    
+
     // Check plain text email.
     $this->assertEqual($text_email['subject'], 'submission label (&>');
     $this->assertEqual($text_email['params']['subject'], 'submission <em>label</em> (&><#)');
diff --git a/web/modules/webform/src/Tests/WebformResultsDisabledTest.php b/web/modules/webform/src/Tests/WebformResultsDisabledTest.php
index 9f956f1e146003ed226a056430586e5fc010ef3c..b1e28c8e803509f9a82edbb2c51ca8cfe0b539be 100644
--- a/web/modules/webform/src/Tests/WebformResultsDisabledTest.php
+++ b/web/modules/webform/src/Tests/WebformResultsDisabledTest.php
@@ -31,14 +31,14 @@ public function testSettings() {
     $this->assertFalse($webform_submission, 'Submission not saved to the database.');
 
     // Check that error message is displayed and form is available for admins.
-    $this->drupalGet('webform/test_form_results_disabled');
+    $this->drupalGet('/webform/test_form_results_disabled');
     $this->assertRaw(t('This webform is currently not saving any submitted data.'));
     $this->assertFieldByName('op', 'Submit');
     $this->assertNoRaw(t('Unable to display this webform. Please contact the site administrator.'));
 
     // Check that error message not displayed and form is disabled for everyone.
     $this->drupalLogout();
-    $this->drupalGet('webform/test_form_results_disabled');
+    $this->drupalGet('/webform/test_form_results_disabled');
     $this->assertNoRaw(t('This webform is currently not saving any submitted data.'));
     $this->assertNoFieldByName('op', 'Submit');
     $this->assertRaw(t('Unable to display this webform. Please contact the site administrator.'));
@@ -49,18 +49,18 @@ public function testSettings() {
     $this->drupalLogin($this->rootUser);
 
     // Check that no error message is displayed and form is available for admins.
-    $this->drupalGet('webform/test_form_results_disabled');
+    $this->drupalGet('/webform/test_form_results_disabled');
     $this->assertNoRaw(t('This webform is currently not saving any submitted data.'));
     $this->assertNoRaw(t('Unable to display this webform. Please contact the site administrator.'));
     $this->assertFieldByName('op', 'Submit');
 
     // Check that results tab is not accessible.
-    $this->drupalGet('admin/structure/webform/manage/test_form_results_disabled/results/submissions');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_results_disabled/results/submissions');
     $this->assertResponse(403);
 
     // Check that error message not displayed and form is enabled for everyone.
     $this->drupalLogout();
-    $this->drupalGet('webform/test_form_results_disabled');
+    $this->drupalGet('/webform/test_form_results_disabled');
     $this->assertNoRaw(t('This webform is currently not saving any submitted data.'));
     $this->assertNoRaw(t('Unable to display this webform. Please contact the site administrator.'));
     $this->assertFieldByName('op', 'Submit');
@@ -73,7 +73,7 @@ public function testSettings() {
     $this->drupalLogin($this->rootUser);
 
     // Check that results tab is accessible.
-    $this->drupalGet('admin/structure/webform/manage/test_form_results_disabled/results/submissions');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_results_disabled/results/submissions');
     $this->assertResponse(200);
 
     // Post a submission.
@@ -81,7 +81,7 @@ public function testSettings() {
     $webform_submission = WebformSubmission::load($sid);
 
     // Check that submission is available.
-    $this->drupalGet('admin/structure/webform/manage/test_form_results_disabled/results/submissions');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_results_disabled/results/submissions');
     $this->assertNoRaw('This webform is currently not saving any submitted data');
     $this->assertRaw('>' . $webform_submission->serial() . '<');
 
@@ -90,7 +90,7 @@ public function testSettings() {
     $webform_results_disabled->save();
 
     // Check that submission is still available with warning.
-    $this->drupalGet('admin/structure/webform/manage/test_form_results_disabled/results/submissions');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_results_disabled/results/submissions');
     $this->assertRaw('This webform is currently not saving any submitted data');
     $this->assertRaw('>' . $webform_submission->serial() . '<');
 
@@ -98,7 +98,7 @@ public function testSettings() {
     $webform_submission->delete();
 
     // Check that results tab is not accessible.
-    $this->drupalGet('admin/structure/webform/manage/test_form_results_disabled/results/submissions');
+    $this->drupalGet('/admin/structure/webform/manage/test_form_results_disabled/results/submissions');
     $this->assertResponse(403);
   }
 
diff --git a/web/modules/webform/src/Tests/WebformResultsExportDownloadTest.php b/web/modules/webform/src/Tests/WebformResultsExportDownloadTest.php
index 9c386bffd5f8ea139b95a261f214d9a342b9d558..63033e32b7728dd6fa7132a1ad37dc8e2a7a091b 100644
--- a/web/modules/webform/src/Tests/WebformResultsExportDownloadTest.php
+++ b/web/modules/webform/src/Tests/WebformResultsExportDownloadTest.php
@@ -28,16 +28,6 @@ class WebformResultsExportDownloadTest extends WebformTestBase {
    */
   protected static $testWebforms = ['test_element_managed_file'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
   /**
    * Tests download files.
    */
diff --git a/web/modules/webform/src/Tests/WebformResultsExportOptionsTest.php b/web/modules/webform/src/Tests/WebformResultsExportOptionsTest.php
index 38df7b1890af96a0c9a491ac03107acdd8c8bc53..ccd11509cffc4c01fb2b4988408dbaeedc9329c6 100644
--- a/web/modules/webform/src/Tests/WebformResultsExportOptionsTest.php
+++ b/web/modules/webform/src/Tests/WebformResultsExportOptionsTest.php
@@ -25,20 +25,16 @@ class WebformResultsExportOptionsTest extends WebformTestBase {
    */
   protected static $testWebforms = ['test_submissions'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
   /**
    * Tests export options.
    */
   public function testExportOptions() {
+    $admin_submission_user = $this->drupalCreateUser([
+      'administer webform submission',
+    ]);
+
+    /**************************************************************************/
+
     /** @var \Drupal\webform\WebformInterface $webform */
     $webform = Webform::load('test_submissions');
     /** @var \Drupal\webform\WebformSubmissionInterface[] $submissions */
@@ -46,7 +42,7 @@ public function testExportOptions() {
     /** @var \Drupal\node\NodeInterface[] $node */
     $nodes = array_values(\Drupal::entityTypeManager()->getStorage('node')->loadByProperties(['type' => 'webform_test_submissions']));
 
-    $this->drupalLogin($this->adminSubmissionUser);
+    $this->drupalLogin($admin_submission_user);
 
     // Check default options.
     $this->getExport($webform);
@@ -131,11 +127,11 @@ public function testExportOptions() {
 
     // Check composite w/o header prefix.
     $this->getExport($webform, ['header_format' => 'label', 'header_prefix' => TRUE]);
-    $this->assertRaw('"Address: Address","Address: Address 2","Address: City/Town","Address: State/Province","Address: Zip/Postal Code","Address: Country"');
+    $this->assertRaw('"Address: Address","Address: Address 2","Address: City/Town","Address: State/Province","Address: ZIP/Postal Code","Address: Country"');
 
     // Check composite w header prefix.
     $this->getExport($webform, ['header_format' => 'label', 'header_prefix' => FALSE]);
-    $this->assertRaw('Address,"Address 2",City/Town,State/Province,"Zip/Postal Code",Country');
+    $this->assertRaw('Address,"Address 2",City/Town,State/Province,"ZIP/Postal Code",Country');
 
     // Check limit.
     $this->getExport($webform, ['range_type' => 'latest', 'range_latest' => 2]);
@@ -170,7 +166,7 @@ public function testExportOptions() {
     $this->assertNoRaw('Hillary,Clinton');
 
     // Check entity type and id hidden.
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/download');
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/download');
     $this->assertNoFieldById('edit-entity-type');
 
     // Change submission 0 & 1 to be submitted user account.
@@ -178,7 +174,7 @@ public function testExportOptions() {
     $submissions[1]->set('entity_type', 'user')->set('entity_id', '2')->save();
 
     // Check entity type and id visible.
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/download');
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/download');
     $this->assertFieldById('edit-entity-type');
 
     // Check entity type limit.
@@ -193,8 +189,9 @@ public function testExportOptions() {
     $this->assertNoRaw('Abraham,Lincoln');
     $this->assertNoRaw('Hillary,Clinton');
 
-    // Check changing default exporter to 'table' settings.
     $this->drupalLogin($this->rootUser);
+
+    // Check changing default exporter to 'table' settings.
     $edit = [
       'exporter' => 'table',
     ];
@@ -203,7 +200,6 @@ public function testExportOptions() {
     $this->assertPattern('#<td>George</td>\s+<td>Washington</td>\s+<td>Male</td>#ms');
 
     // Check changing default export (delimiter) settings.
-    $this->drupalLogin($this->rootUser);
     $edit = [
       'exporter' => 'delimited',
       'exporters[delimited][delimiter]' => '|',
diff --git a/web/modules/webform/src/Tests/WebformSubmissionAccessTest.php b/web/modules/webform/src/Tests/WebformSubmissionAccessTest.php
deleted file mode 100644
index 49fe4ca4484175483854c15f8fa0abf9913600b2..0000000000000000000000000000000000000000
--- a/web/modules/webform/src/Tests/WebformSubmissionAccessTest.php
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-
-namespace Drupal\webform\Tests;
-
-use Drupal\webform\Entity\Webform;
-
-/**
- * Tests for webform submission access.
- *
- * @group Webform
- */
-class WebformSubmissionAccessTest extends WebformTestBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
-  /**
-   * Test webform submission access permissions.
-   */
-  public function testPermissions() {
-    global $base_path;
-
-    $webform_id = 'contact';
-    $webform = Webform::load('contact');
-
-    /**************************************************************************/
-    // Own submission permissions (authenticated).
-    /**************************************************************************/
-
-    $this->drupalLogin($this->ownWebformSubmissionUser);
-
-    $edit = ['subject' => '{subject}', 'message' => '{message}'];
-    $sid_1 = $this->postSubmission($webform, $edit);
-
-    // Check view own previous submission message.
-    $this->drupalGet('webform/' . $webform->id());
-    $this->assertRaw('You have already submitted this webform.');
-    $this->assertRaw("<a href=\"{$base_path}webform/{$webform_id}/submissions/{$sid_1}\">View your previous submission</a>.");
-
-    // Check 'view own submission' permission.
-    $this->drupalGet("webform/{$webform_id}/submissions/{$sid_1}");
-    $this->assertResponse(200);
-
-    // Check 'edit own submission' permission.
-    $this->drupalGet("webform/{$webform_id}/submissions/{$sid_1}/edit");
-    $this->assertResponse(200);
-
-    // Check 'delete own submission' permission.
-    $this->drupalGet("webform/{$webform_id}/submissions/{$sid_1}/delete");
-    $this->assertResponse(200);
-
-    $sid_2 = $this->postSubmission($webform, $edit);
-
-    // Check view own previous submissions message.
-    $this->drupalGet('webform/' . $webform->id());
-    $this->assertRaw('You have already submitted this webform.');
-    $this->assertRaw("<a href=\"{$base_path}webform/{$webform_id}/submissions\">View your previous submissions</a>");
-
-    // Check view own previous submissions.
-    $this->drupalGet("webform/{$webform_id}/submissions");
-    $this->assertResponse(200);
-    $this->assertLinkByHref("{$base_path}webform/{$webform_id}/submissions/{$sid_1}");
-    $this->assertLinkByHref("{$base_path}webform/{$webform_id}/submissions/{$sid_2}");
-
-    // Check webform submission allowed.
-    $this->drupalGet("/admin/structure/webform/manage/{$webform_id}/submission/{$sid_1}");
-    $this->assertResponse(200);
-
-    // Check webform results access denied.
-    $this->drupalGet("/admin/structure/webform/manage/{$webform_id}/results/submissions");
-    $this->assertResponse(403);
-
-    // Check all results access denied.
-    $this->drupalGet('/admin/structure/webform/submissions/manage');
-    $this->assertResponse(403);
-
-    /**************************************************************************/
-    // Any submission permissions.
-    /**************************************************************************/
-
-    // Login as any user.
-    $this->drupalLogin($this->anyWebformSubmissionUser);
-
-    // Check webform results access allowed.
-    $this->drupalGet("/admin/structure/webform/manage/{$webform_id}/results/submissions");
-    $this->assertResponse(200);
-    $this->assertLinkByHref("{$base_path}admin/structure/webform/manage/{$webform_id}/submission/{$sid_1}");
-    $this->assertLinkByHref("{$base_path}admin/structure/webform/manage/{$webform_id}/submission/{$sid_2}");
-
-    // Check webform submission access allowed.
-    $this->drupalGet("/admin/structure/webform/manage/{$webform_id}/submission/{$sid_1}");
-    $this->assertResponse(200);
-
-    // Check all results access allowed.
-    $this->drupalGet('/admin/structure/webform/submissions/manage');
-    $this->assertResponse(200);
-
-    /**************************************************************************/
-    // Own submission permissions (anonymous).
-    /**************************************************************************/
-
-    $this->addWebformSubmissionOwnPermissionsToAnonymous();
-    $this->drupalLogout();
-
-    $edit = ['name' => '{name}', 'email' => 'example@example.com', 'subject' => '{subject}', 'message' => '{message}'];
-    $sid_1 = $this->postSubmission($webform, $edit);
-
-    // Check view own previous submission message.
-    $this->drupalGet('webform/' . $webform->id());
-    $this->assertRaw('You have already submitted this webform.');
-    $this->assertRaw("<a href=\"{$base_path}webform/{$webform_id}/submissions/{$sid_1}\">View your previous submission</a>.");
-
-    // Check 'view own submission' permission.
-    $this->drupalGet("webform/{$webform_id}/submissions/{$sid_1}");
-    $this->assertResponse(200);
-
-    // Check 'edit own submission' permission.
-    $this->drupalGet("webform/{$webform_id}/submissions/{$sid_1}/edit");
-    $this->assertResponse(200);
-
-    // Check 'delete own submission' permission.
-    $this->drupalGet("webform/{$webform_id}/submissions/{$sid_1}/delete");
-    $this->assertResponse(200);
-
-    $sid_2 = $this->postSubmission($webform, $edit);
-
-    // Check view own previous submissions message.
-    $this->drupalGet('webform/' . $webform->id());
-    $this->assertRaw('You have already submitted this webform.');
-    $this->assertRaw("<a href=\"{$base_path}webform/{$webform_id}/submissions\">View your previous submissions</a>");
-
-    // Check view own previous submissions.
-    $this->drupalGet("webform/{$webform_id}/submissions");
-    $this->assertResponse(200);
-    $this->assertLinkByHref("{$base_path}webform/{$webform_id}/submissions/{$sid_1}");
-    $this->assertLinkByHref("{$base_path}webform/{$webform_id}/submissions/{$sid_2}");
-  }
-
-}
diff --git a/web/modules/webform/src/Tests/WebformSubmissionApiTest.php b/web/modules/webform/src/Tests/WebformSubmissionApiTest.php
index 34813c56dbdbd23cd2614c52b90f4260ff10a4e6..deb81212cc18005859535ca2116d32edff1704de 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionApiTest.php
+++ b/web/modules/webform/src/Tests/WebformSubmissionApiTest.php
@@ -21,27 +21,18 @@ class WebformSubmissionApiTest extends WebformTestBase {
    */
   protected static $testWebforms = ['test_form_wizard_advanced', 'test_form_limit'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
   /**
    * Test webform API.
    */
   public function testApi() {
+    $normal_user = $this->drupalCreateUser();
+
+    $contact_webform = Webform::load('contact');
 
     /**************************************************************************/
     // Basic form.
     /**************************************************************************/
 
-    $contact_webform = Webform::load('contact');
-
     // Check submitting a simple webform.
     $values = [
       'webform_id' => 'contact',
@@ -104,7 +95,7 @@ public function testApi() {
 
     $test_form_wizard_advanced_webform = Webform::load('test_form_wizard_advanced');
 
-    // Check submitting a multistep form with required fields.
+    // Check submitting a multi-step form with required fields.
     $values = [
       'webform_id' => 'test_form_wizard_advanced',
       'data' => [
@@ -119,7 +110,7 @@ public function testApi() {
     $webform_submission = WebformSubmissionForm::submitFormValues($values);
     $this->assertEqual($webform_submission->id(), $this->getLastSubmissionId($test_form_wizard_advanced_webform));
 
-    // Check validating a multistep form with required fields.
+    // Check validating a multi-step form with required fields.
     $values = [
       'webform_id' => 'test_form_wizard_advanced',
       'data' => [
@@ -133,7 +124,7 @@ public function testApi() {
       'email' => 'The email address <em class="placeholder">invalid</em> is not valid.',
     ]);
 
-    // Check validating a multistep form with invalid #options.
+    // Check validating a multi-step form with invalid #options.
     $values = [
       'webform_id' => 'test_form_wizard_advanced',
       'data' => [
@@ -149,14 +140,14 @@ public function testApi() {
     WebformElementHelper::convertRenderMarkupToStrings($errors);
     // $this->debug($errors);
     $this->assertEqual($errors, [
-        'gender' => 'An illegal choice has been detected. Please contact the site administrator.',
+      'gender' => 'An illegal choice has been detected. Please contact the site administrator.',
     ]);
 
     /**************************************************************************/
     // Submission limit form.
     /**************************************************************************/
 
-    $this->drupalLogin($this->normalUser);
+    $this->drupalLogin($normal_user);
 
     $test_form_limit_webform = Webform::load('test_form_limit');
 
@@ -190,7 +181,7 @@ public function testApi() {
     // Check form closed message.
     $test_form_limit_webform->setStatus(FALSE)->save();
     $result = WebformSubmissionForm::isOpen($test_form_limit_webform);
-    $this->assertEqual($result['#markup'], 'Sorry...This form is closed to new submissions.');
+    $this->assertEqual($result['#markup'], 'Sorry…This form is closed to new submissions.');
   }
 
 }
diff --git a/web/modules/webform/src/Tests/WebformSubmissionGenerateTest.php b/web/modules/webform/src/Tests/WebformSubmissionGenerateTest.php
index 5bac11bd030572a4c67c9955222ea09d7b2584d9..809b305c46ee5ed32a258eeda15e1d4a48776c13 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionGenerateTest.php
+++ b/web/modules/webform/src/Tests/WebformSubmissionGenerateTest.php
@@ -37,7 +37,7 @@ public function testWebformSubmissionGenerate() {
     $this->assertEqual($data['subject'], $test_data['subject']);
 
     // Check test form classes and values.
-    $this->drupalGet('webform/contact/test');
+    $this->drupalGet('/webform/contact/test');
     $this->assertCssSelect('.webform-submission-form.webform-submission-test-form.webform-submission-contact-form.webform-submission-contact-test-form');
     foreach ($test_data as $name => $value) {
       $this->assertFieldByName($name, $value);
@@ -48,14 +48,14 @@ public function testWebformSubmissionGenerate() {
     /**************************************************************************/
 
     // Check add form classes and empty values.
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertCssSelect('.webform-submission-form.webform-submission-add-form.webform-submission-contact-form.webform-submission-contact-add-form');
     foreach ($test_data as $name => $value) {
       $this->assertNoFieldByName($name, $value);
     }
 
     // Check add form classes and values with querystring parameter.
-    $this->drupalGet('webform/contact', ['query' => ['_webform_test' => 'contact']]);
+    $this->drupalGet('/webform/contact', ['query' => ['_webform_test' => 'contact']]);
     $this->assertCssSelect('.webform-submission-form.webform-submission-test-form.webform-submission-contact-form.webform-submission-contact-test-form');
     foreach ($test_data as $name => $value) {
       $this->assertFieldByName($name, $value);
diff --git a/web/modules/webform/src/Tests/WebformSubmissionListBuilderTest.php b/web/modules/webform/src/Tests/WebformSubmissionListBuilderTest.php
index e77df1fa07576a436a48732c9e796130ce71ea84..200a6100bf68422f137f6b2a92e9b73134f388c7 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionListBuilderTest.php
+++ b/web/modules/webform/src/Tests/WebformSubmissionListBuilderTest.php
@@ -25,29 +25,37 @@ class WebformSubmissionListBuilderTest extends WebformTestBase {
    */
   protected static $testWebforms = ['test_submissions'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
   /**
    * Tests results.
    */
   public function testResults() {
     global $base_path;
 
+    $admin_user = $this->drupalCreateUser([
+      'administer webform',
+    ]);
+
+    $own_submission_user = $this->drupalCreateUser([
+      'view own webform submission',
+      'edit own webform submission',
+      'delete own webform submission',
+      'access webform submission user',
+    ]);
+
+    $admin_submission_user = $this->drupalCreateUser([
+      'administer webform submission',
+    ]);
+
     /** @var \Drupal\webform\WebformInterface $webform */
     $webform = Webform::load('test_submissions');
+
     /** @var \Drupal\webform\WebformSubmissionInterface[] $submissions */
     $submissions = array_values(\Drupal::entityTypeManager()->getStorage('webform_submission')->loadByProperties(['webform_id' => 'test_submissions']));
 
-    // Login the normal user.
-    $this->drupalLogin($this->ownWebformSubmissionUser);
+    /**************************************************************************/
+
+    // Login the own submission user.
+    $this->drupalLogin($own_submission_user);
 
     // Make the second submission to be starred (aka sticky).
     $submissions[1]->setSticky(TRUE)->save();
@@ -55,11 +63,11 @@ public function testResults() {
     // Make the third submission to be locked.
     $submissions[2]->setLocked(TRUE)->save();
 
-    $this->drupalLogin($this->adminSubmissionUser);
+    $this->drupalLogin($admin_submission_user);
 
     /* Filter */
 
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
 
     // Check state options with totals.
     $this->assertRaw('<select data-drupal-selector="edit-state" id="edit-state" name="state" class="form-select"><option value="" selected="selected">All [4]</option><option value="starred">Starred [1]</option><option value="unstarred">Unstarred [3]</option><option value="locked">Locked [1]</option><option value="unlocked">Unlocked [3]</option></select>');
@@ -73,9 +81,16 @@ public function testResults() {
     $this->assertRaw($submissions[2]->getElementData('first_name'));
     $this->assertNoFieldById('edit-reset', 'reset');
 
+    // Check results filtered by uuid.
+    $this->drupalPostForm('admin/structure/webform/manage/' . $webform->id() . '/results/submissions', ['search' => $submissions[0]->get('uuid')->value], t('Filter'));
+    $this->assertUrl('admin/structure/webform/manage/' . $webform->id() . '/results/submissions?search=' . $submissions[0]->get('uuid')->value);
+    $this->assertRaw($submissions[0]->getElementData('first_name'));
+    $this->assertNoRaw($submissions[1]->getElementData('first_name'));
+    $this->assertNoRaw($submissions[2]->getElementData('first_name'));
+
     // Check results filtered by key(word).
     $this->drupalPostForm('admin/structure/webform/manage/' . $webform->id() . '/results/submissions', ['search' => $submissions[0]->getElementData('first_name')], t('Filter'));
-    $this->assertUrl('admin/structure/webform/manage/' . $webform->id() . '/results/submissions?search=' . $submissions[0]->getElementData('first_name') . '&state=');
+    $this->assertUrl('admin/structure/webform/manage/' . $webform->id() . '/results/submissions?search=' . $submissions[0]->getElementData('first_name'));
     $this->assertRaw($submissions[0]->getElementData('first_name'));
     $this->assertNoRaw($submissions[1]->getElementData('first_name'));
     $this->assertNoRaw($submissions[2]->getElementData('first_name'));
@@ -83,7 +98,7 @@ public function testResults() {
 
     // Check results filtered by state:starred.
     $this->drupalPostForm('admin/structure/webform/manage/' . $webform->id() . '/results/submissions', ['state' => 'starred'], t('Filter'));
-    $this->assertUrl('admin/structure/webform/manage/' . $webform->id() . '/results/submissions?search=&state=starred');
+    $this->assertUrl('admin/structure/webform/manage/' . $webform->id() . '/results/submissions?state=starred');
     $this->assertRaw('<option value="starred" selected="selected">Starred [1]</option>');
     $this->assertNoRaw($submissions[0]->getElementData('first_name'));
     $this->assertRaw($submissions[1]->getElementData('first_name'));
@@ -92,7 +107,7 @@ public function testResults() {
 
     // Check results filtered by state:starred.
     $this->drupalPostForm('admin/structure/webform/manage/' . $webform->id() . '/results/submissions', ['state' => 'locked'], t('Filter'));
-    $this->assertUrl('admin/structure/webform/manage/' . $webform->id() . '/results/submissions?search=&state=locked');
+    $this->assertUrl('admin/structure/webform/manage/' . $webform->id() . '/results/submissions?state=locked');
     $this->assertRaw('<option value="locked" selected="selected">Locked [1]</option>');
     $this->assertNoRaw($submissions[0]->getElementData('first_name'));
     $this->assertNoRaw($submissions[1]->getElementData('first_name'));
@@ -104,17 +119,17 @@ public function testResults() {
     /**************************************************************************/
 
     // Check that access is denied to custom results table.
-    $this->drupalLogin($this->adminSubmissionUser);
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/submissions/custom');
+    $this->drupalLogin($admin_submission_user);
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/submissions/custom');
     $this->assertResponse(403);
 
     // Check that access is allowed to custom results table.
-    $this->drupalLogin($this->adminWebformUser);
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/submissions/custom');
+    $this->drupalLogin($admin_user);
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/submissions/custom');
     $this->assertResponse(200);
 
     // Check that created is visible and changed is hidden.
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
     $this->assertRaw('sort by Created');
     $this->assertNoRaw('sort by Changed');
 
@@ -124,12 +139,17 @@ public function testResults() {
     // Check that no pager is being displayed.
     $this->assertNoRaw('<nav class="pager" role="navigation" aria-labelledby="pagination-heading">');
 
-    // Check that table is sorted by serial.
-    $this->assertRaw('<th specifier="serial" aria-sort="descending" class="is-active">');
+    // Check that table is sorted by created.
+    $this->assertRaw('<th specifier="created" class="priority-medium is-active" aria-sort="descending">');
 
     // Check the table results order by sid.
     $this->assertPattern('#Hillary.+Abraham.+George#ms');
 
+    // Check the table links to canonical view.
+    $this->assertRaw('data-webform-href="' . $submissions[0]->toUrl()->toString() . '"');
+    $this->assertRaw('data-webform-href="' . $submissions[1]->toUrl()->toString() . '"');
+    $this->assertRaw('data-webform-href="' . $submissions[2]->toUrl()->toString() . '"');
+
     // Customize to results table.
     $edit = [
       'columns[created][checkbox]' => FALSE,
@@ -139,12 +159,19 @@ public function testResults() {
       'sort' => 'element__first_name',
       'direction' => 'desc',
       'limit' => 20,
+      'link_type' => 'table',
     ];
     $this->drupalPostForm('admin/structure/webform/manage/' . $webform->id() . '/results/submissions/custom', $edit, t('Save'));
     $this->assertRaw('The customized table has been saved.');
 
+    // Check that table now link to table.
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
+    $this->assertRaw('data-webform-href="' . $submissions[0]->toUrl('table')->toString() . '"');
+    $this->assertRaw('data-webform-href="' . $submissions[1]->toUrl('table')->toString() . '"');
+    $this->assertRaw('data-webform-href="' . $submissions[2]->toUrl('table')->toString() . '"');
+
     // Check that sid is hidden and changed is visible.
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
     $this->assertNoRaw('sort by Created');
     $this->assertRaw('sort by Changed');
 
@@ -158,7 +185,7 @@ public function testResults() {
     $webform->setState('results.custom.limit', 1);
 
     // Check that only one result (Hillary #2) is displayed with pager.
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
     $this->assertNoRaw('George');
     $this->assertNoRaw('Abraham');
     $this->assertNoRaw('Hillary');
@@ -169,7 +196,7 @@ public function testResults() {
     $webform->setState('results.custom.limit', 20);
 
     // Check Header label and element value display.
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
 
     // Check user header and value.
     $this->assertRaw('<a href="' . $base_path . 'admin/structure/webform/manage/' . $webform->id() . '/results/submissions?sort=asc&amp;order=User" title="sort by User">User</a>');
@@ -185,7 +212,7 @@ public function testResults() {
       'element_format' => 'raw',
     ]);
 
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
 
     // Check user header and value.
     $this->assertRaw('<a href="' . $base_path . 'admin/structure/webform/manage/' . $webform->id() . '/results/submissions?sort=asc&amp;order=uid" title="sort by uid">uid</a>');
@@ -199,22 +226,22 @@ public function testResults() {
     // Customize user results.
     /**************************************************************************/
 
-    $this->drupalLogin($this->ownWebformSubmissionUser);
+    $this->drupalLogin($own_submission_user);
 
     // Check view own submissions.
     $this->drupalget('/webform/' . $webform->id() . '/submissions');
-    $this->assertRaw('<th specifier="serial" aria-sort="descending" class="is-active">');
-    $this->assertRaw('<th specifier="created" class="priority-medium">');
+    $this->assertRaw('<th specifier="serial">');
+    $this->assertRaw('<th specifier="created" class="priority-medium is-active" aria-sort="descending">');
     $this->assertRaw('<th specifier="remote_addr" class="priority-low">');
 
-    // Display on first name and last name columns.
+    // Display only first name and last name columns.
     $webform->setSetting('submission_user_columns', ['element__first_name', 'element__last_name'])
       ->save();
 
     // Check view own submissions only include first name and last name.
     $this->drupalget('/webform/' . $webform->id() . '/submissions');
-    $this->assertNoRaw('<th specifier="serial" aria-sort="descending" class="is-active">');
-    $this->assertNoRaw('<th specifier="created" class="priority-medium">');
+    $this->assertNoRaw('<th specifier="serial">');
+    $this->assertNoRaw('<th specifier="created" class="priority-medium is-active" aria-sort="descending">');
     $this->assertNoRaw('<th specifier="remote_addr" class="priority-low">');
     $this->assertRaw('<th specifier="element__first_name" aria-sort="ascending" class="is-active">');
     $this->assertRaw('<th specifier="element__last_name">');
diff --git a/web/modules/webform/src/Tests/WebformSubmissionStorageTest.php b/web/modules/webform/src/Tests/WebformSubmissionStorageTest.php
index d81e3b9b424776ec1c485cfb26b2db79c41bf0ac..98ac360b35e4955a612eb1702f00a6cd73663e96 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionStorageTest.php
+++ b/web/modules/webform/src/Tests/WebformSubmissionStorageTest.php
@@ -59,15 +59,19 @@ public function testSubmissionStorage() {
     $this->assertEqual($storage->getTotal($webform, NULL, $user2), 3);
 
     // Check next submission.
+    $this->drupalLogin($user1);
     $this->assertEqual($storage->getNextSubmission($user1_submissions[0], NULL, $user1)->id(), $user1_submissions[1]->id(), "User 1 can navigate forward to user 1's next submission");
     $this->assertNull($storage->getNextSubmission($user1_submissions[2], NULL, $user1), "User 1 can't navigate forward to user 2's next submission");
+    $this->drupalLogin($user2);
     $this->assertNull($storage->getNextSubmission($user2_submissions[0], NULL, $user2), "User 2 can't navigate forward to user 2's next submission because of missing 'view own webform submission' permission");
     $this->drupalLogin($admin_user);
     $this->assertEqual($storage->getNextSubmission($user1_submissions[2], NULL)->id(), $user2_submissions[0]->id(), "Admin user can navigate between user submissions");
     $this->drupalLogout();
 
     // Check previous submission.
+    $this->drupalLogin($user1);
     $this->assertEqual($storage->getPreviousSubmission($user1_submissions[1], NULL, $user1)->id(), $user1_submissions[0]->id(), "User 1 can navigate backward to user 1's previous submission");
+    $this->drupalLogin($user2);
     $this->assertNull($storage->getPreviousSubmission($user2_submissions[0], NULL, $user2), "User 2 can't navigate backward to user 1's previous submission");
     $this->drupalLogin($admin_user);
     $this->assertEqual($storage->getPreviousSubmission($user2_submissions[0], NULL)->id(), $user1_submissions[2]->id(), "Admin user can navigate between user submissions");
diff --git a/web/modules/webform/src/Tests/WebformSubmissionTest.php b/web/modules/webform/src/Tests/WebformSubmissionTest.php
index 536dbe3a7578f7605bfac1a1958bcf35e1fcb1fa..192fa4450f49b447e2c0b4f042b3642cd1e842f9 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionTest.php
+++ b/web/modules/webform/src/Tests/WebformSubmissionTest.php
@@ -30,37 +30,41 @@ class WebformSubmissionTest extends WebformTestBase {
    * Tests webform submission entity.
    */
   public function testWebformSubmission() {
+    $normal_user = $this->drupalCreateUser();
+
     /** @var \Drupal\webform\WebformInterface $webform */
     $webform = Webform::load('test_submissions');
+
     /** @var \Drupal\webform\WebformSubmissionInterface[] $submissions */
     $submissions = array_values(\Drupal::entityTypeManager()->getStorage('webform_submission')->loadByProperties(['webform_id' => 'test_submissions']));
 
     /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
     $webform_submission = reset($submissions);
 
+    /**************************************************************************/
+
     // Check create submission.
     $this->assert($webform_submission instanceof WebformSubmission, '$webform_submission instanceof WebformSubmission');
 
     // Check get webform.
     $this->assertEqual($webform_submission->getWebform()->id(), $webform->id());
 
-    // Check that YAML source entity is NULL.
+    // Check that source entity is NULL.
     $this->assertNull($webform_submission->getSourceEntity());
 
-    // Check get YAML source URL without uri, which will still return
+    // Check getting source URL without uri, which will still return
     // the webform.
     $webform_submission
       ->set('uri', NULL)
       ->save();
     $this->assertEqual($webform_submission->getSourceUrl()->toString(), $webform->toUrl('canonical', ['absolute' => TRUE])->toString());
 
-    // Check get YAML source URL set to user 1.
-    $this->createUsers();
+    // Check get source URL set to user 1.
     $webform_submission
       ->set('entity_type', 'user')
-      ->set('entity_id', $this->normalUser->id())
+      ->set('entity_id', $normal_user->id())
       ->save();
-    $this->assertEqual($webform_submission->getSourceUrl()->toString(), $this->normalUser->toUrl('canonical', ['absolute' => TRUE])->toString());
+    $this->assertEqual($webform_submission->getSourceUrl()->toString(), $normal_user->toUrl('canonical', ['absolute' => TRUE])->toString());
 
     // Check missing webform_id exception.
     try {
@@ -78,14 +82,27 @@ public function testWebformSubmission() {
     // Check submission label.
     $webform_submission->save();
     $this->assertEqual($webform_submission->label(), $webform->label() . ': Submission #' . $webform_submission->serial());
+
+    // Check test submission URI.
+    // @see \Drupal\webform\WebformSubmissionForm::save
+    $this->drupalLogin($this->rootUser);
+    $sid = $this->postSubmissionTest($webform);
+    $webform_submission = WebformSubmission::load($sid);
+    $this->assertEqual($webform_submission->getSourceUrl()->toString(), $webform->toUrl('canonical', ['absolute' => TRUE])->toString());
+    $this->drupalLogout();
   }
 
   /**
    * Tests duplicating webform submission.
    */
   public function testDuplicate() {
-    $this->createUsers();
-    $this->drupalLogin($this->adminSubmissionUser);
+    $admin_submission_user = $this->drupalCreateUser([
+      'administer webform submission',
+    ]);
+
+    /**************************************************************************/
+
+    $this->drupalLogin($admin_submission_user);
 
     $webform = Webform::load('contact');
     $sid = $this->postSubmission($webform, [
diff --git a/web/modules/webform/src/Tests/WebformSubmissionTokenUpdateTest.php b/web/modules/webform/src/Tests/WebformSubmissionTokenUpdateTest.php
index 070fa5d91c99cae9b1e9d995ea35c832898b75f2..6f904baca37d29127156446f1e69a9a271e17d2b 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionTokenUpdateTest.php
+++ b/web/modules/webform/src/Tests/WebformSubmissionTokenUpdateTest.php
@@ -19,29 +19,23 @@ class WebformSubmissionTokenUpdateTest extends WebformTestBase {
    */
   protected static $testWebforms = ['test_token_update'];
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    // Create users.
-    $this->createUsers();
-  }
-
   /**
    * Test updating webform submission using tokenized URL.
    */
   public function testTokenUpdateTest() {
+    $normal_user = $this->drupalCreateUser();
+
     $webform = Webform::load('test_token_update');
 
+    /**************************************************************************/
+
     // Post test submission.
     $this->drupalLogin($this->rootUser);
     $sid = $this->postSubmissionTest($webform);
     $webform_submission = WebformSubmission::load($sid);
 
     // Check token update access allowed.
-    $this->drupalLogin($this->normalUser);
+    $this->drupalLogin($normal_user);
     $this->drupalGet($webform_submission->getTokenUrl());
     $this->assertResponse(200);
     $this->assertRaw('Submission information');
@@ -49,7 +43,7 @@ public function testTokenUpdateTest() {
 
     // Check token update access denied.
     $webform->setSetting('token_update', FALSE)->save();
-    $this->drupalLogin($this->normalUser);
+    $this->drupalLogin($normal_user);
     $this->drupalGet($webform_submission->getTokenUrl());
     $this->assertResponse(200);
     $this->assertNoRaw('Submission information');
@@ -66,7 +60,7 @@ public function testTokenUpdateTest() {
     $webform->save();
 
     // Check that access is denied for anonymous user.
-    $this->drupalGet('webform/test_token_update');
+    $this->drupalGet('/webform/test_token_update');
     $this->assertResponse(403);
 
     // Check token update access allowed for anonymous user.
diff --git a/web/modules/webform/src/Tests/WebformSubmissionViewTest.php b/web/modules/webform/src/Tests/WebformSubmissionViewTest.php
index 3a8f61b2fba582fd69d8d0ef24eb264878b137a2..0211b516a73dbfe04a9d7d70ce06172fdaf7b893 100644
--- a/web/modules/webform/src/Tests/WebformSubmissionViewTest.php
+++ b/web/modules/webform/src/Tests/WebformSubmissionViewTest.php
@@ -34,9 +34,6 @@ class WebformSubmissionViewTest extends WebformTestBase {
   public function setUp() {
     parent::setUp();
 
-    // Create users.
-    $this->createUsers();
-
     // Create filters.
     $this->createFilters();
   }
@@ -45,21 +42,28 @@ public function setUp() {
    * Tests view submissions.
    */
   public function testView() {
+    $admin_submission_user = $this->drupalCreateUser([
+      'administer webform submission',
+    ]);
+
+    /**************************************************************************/
+
     $account = User::load(1);
 
     $webform_element = Webform::load('test_element');
     $sid = $this->postSubmission($webform_element);
     $submission = WebformSubmission::load($sid);
 
-    $this->drupalLogin($this->adminSubmissionUser);
+    $this->drupalLogin($admin_submission_user);
 
-    $this->drupalGet('admin/structure/webform/manage/test_element/submission/' . $submission->id());
+    $this->drupalGet('/admin/structure/webform/manage/test_element/submission/' . $submission->id());
 
     // Check displayed values.
     $elements = [
       'hidden' => '{hidden}',
       'value' => '{value}',
       'textarea' => "{textarea line 1}<br />\n{textarea line 2}",
+      'empty' => '{Empty}',
       'textfield' => '{textfield}',
       'select' => 'one',
       'select_multiple' => 'one, two',
diff --git a/web/modules/webform/src/Tests/WebformSubmissionViewsTest.php b/web/modules/webform/src/Tests/WebformSubmissionViewsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1b5911e5eaef584d7c3845b433454fa71264d0b2
--- /dev/null
+++ b/web/modules/webform/src/Tests/WebformSubmissionViewsTest.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace Drupal\webform\Tests;
+
+use Drupal\webform\Entity\Webform;
+
+/**
+ * Tests for webform submission views integration.
+ *
+ * @group Webform
+ */
+class WebformSubmissionViewsTest extends WebformTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['views', 'webform'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_submission_views'];
+
+  /**
+   * Tests submissions views.
+   */
+  public function testSubmissionViewsAccess() {
+    // Check administer view.
+    $user = $this->drupalCreateUser(['administer webform submission']);
+    $this->drupalLogin($user);
+    $this->drupalGet('/admin/structure/webform/manage/test_submission_views/results/submissions');
+    $this->assertRaw('view-id-webform_submissions view-display-id-embed_administer');
+
+    // Check 200 response.
+    $this->drupalGet('/admin/structure/webform/manage/test_submission_views/results/submissions/admin');
+    $this->assertResponse(200);
+
+    // Check manage view.
+    $user = $this->drupalCreateUser(['edit any webform submission', 'view any webform submission']);
+    $this->drupalLogin($user);
+    $this->drupalGet('/admin/structure/webform/manage/test_submission_views/results/submissions');
+    $this->assertRaw('view-id-webform_submissions view-display-id-embed_manage');
+
+    // Check 403 access denied response.
+    $this->drupalGet('/admin/structure/webform/manage/test_submission_views/results/submissions/admin');
+    $this->assertResponse(403);
+
+    // Check 404 not found response.
+    $this->drupalGet('/admin/structure/webform/manage/test_submission_views/results/submissions/not_found');
+    $this->assertResponse(404);
+
+    // Check review view.
+    $user = $this->drupalCreateUser(['view any webform submission']);
+    $this->drupalLogin($user);
+    $this->drupalGet('/admin/structure/webform/manage/test_submission_views/results/submissions');
+    $this->assertRaw('view-id-webform_submissions view-display-id-embed_review');
+  }
+
+  /**
+   * Tests submissions views.
+   */
+  public function testSubmissionViews() {
+    $uid = $this->rootUser->id();
+    $this->drupalLogin($this->rootUser);
+
+    /**************************************************************************/
+    // Global.
+    /**************************************************************************/
+
+    // Setup global submissions and user submissions views.
+    \Drupal::configFactory()->getEditable('webform.settings')
+      ->set('settings.default_submission_views', [
+        'global' => [
+          'view' => 'webform_submissions:embed_default',
+          'title' => 'Global submissions',
+          'global_routes' => ['entity.webform_submission.collection'],
+          'webform_routes' => [],
+          'node_routes' => [],
+        ],
+        'user' => [
+          'view' => 'webform_submissions:embed_default',
+          'title' => 'User submissions',
+          'global_routes' => ['entity.webform_submission.user'],
+          'webform_routes' => [],
+          'node_routes' => [],
+        ],
+      ])
+      ->save();
+
+    // Check global submissions entity list is replaced by the view.
+    $this->drupalGet('/admin/structure/webform/submissions/manage');
+    $this->assertNoRaw('webform-results-table');
+    $this->assertRaw('view-id-webform_submissions view-display-id-embed_default');
+
+    // Check user submissions entity list is replaced by the view.
+    $this->drupalGet("/user/$uid/submissions");
+    $this->assertNoRaw('webform-results-table');
+    $this->assertRaw('view-id-webform_submissions view-display-id-embed_default');
+
+    // Clear global submission views replace.
+    \Drupal::configFactory()
+      ->getEditable('webform.settings')
+      ->set('settings.default_submission_views_replace.global_routes', [])
+      ->save();
+
+    // Check global submissions entity list is displayed.
+    $this->drupalGet('/admin/structure/webform/submissions/manage');
+    $this->assertRaw('webform-results-table');
+    $this->assertNoRaw('view-id-webform_submissions view-display-id-embed_default');
+
+    // Check global submissions views is moved to dedicated path.
+    $this->clickLink('Global submissions');
+    $this->assertUrl('/admin/structure/webform/submissions/manage/global');
+    $this->assertNoRaw('webform-results-table');
+    $this->assertRaw('view-id-webform_submissions view-display-id-embed_default');
+
+    // Check user submissions entity list is displayed.
+    $this->drupalGet("/user/$uid/submissions");
+    $this->assertRaw('webform-results-table');
+    $this->assertNoRaw('view-id-webform_submissions view-display-id-embed_default');
+
+    // Check user submissions entity list is moved to dedicated path.
+    $this->clickLink('User submissions');
+    $this->assertUrl("/user/$uid/submissions/user");
+    $this->assertNoRaw('webform-results-table');
+    $this->assertRaw('view-id-webform_submissions view-display-id-embed_default');
+
+    /**************************************************************************/
+    // Webform.
+    /**************************************************************************/
+
+    // Post a submission and save a draft.
+    $webform = Webform::load('test_submission_views');
+    $this->postSubmission($webform);
+    $this->postSubmission($webform);
+    $this->postSubmission($webform, [], t('Save Draft'));
+    $this->postSubmission($webform, [], t('Save Draft'));
+
+    // Check webform submissions views.
+    $this->drupalGet('/admin/structure/webform/manage/test_submission_views/results/submissions');
+    $this->assertNoRaw('webform-results-table');
+    $this->assertRaw('view-id-webform_submissions view-display-id-embed_administer');
+
+    // Check webform user views.
+    $this->drupalGet('/webform/test_submission_views/drafts');
+    $this->assertNoRaw('webform-results-table');
+    $this->assertRaw('view-id-webform_submissions view-display-id-embed_default');
+
+    // Clear global webform views replace.
+    \Drupal::configFactory()
+      ->getEditable('webform.settings')
+      ->set('settings.default_submission_views_replace.webform_routes', [])
+      ->save();
+
+    // Check webform submissions entity list is displayed.
+    $this->drupalGet('/admin/structure/webform/manage/test_submission_views/results/submissions');
+    $this->assertRaw('webform-results-table');
+    $this->assertNoRaw('view-id-webform_submissions view-display-id-embed_administer');
+
+    // Check webform submissions views is moved to dedicated path.
+    $this->clickLink('Administer submissions');
+    $this->assertUrl('/admin/structure/webform/manage/test_submission_views/results/submissions/admin');
+    $this->assertNoRaw('webform-results-table');
+    $this->assertRaw('view-id-webform_submissions view-display-id-embed_administer');
+
+    // Check webform submissions entity list is displayed.
+    $this->drupalGet('/webform/test_submission_views/drafts');
+    $this->assertRaw('webform-results-table');
+    $this->assertNoRaw('view-id-webform_submissions view-display-id-embed_default');
+
+    // Check webform submissions entity list is moved to dedicated path.
+    $this->clickLink('User submissions');
+    $this->assertUrl('/webform/test_submission_views/drafts/user');
+    $this->assertNoRaw('webform-results-table');
+    $this->assertRaw('view-id-webform_submissions view-display-id-embed_default');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/WebformTestBase.php b/web/modules/webform/src/Tests/WebformTestBase.php
index 55713b129840bf7276bc02bee67c4b791cff6323..5dc15cd6e8c5a1d1845e7797616a8f53c8f42289 100644
--- a/web/modules/webform/src/Tests/WebformTestBase.php
+++ b/web/modules/webform/src/Tests/WebformTestBase.php
@@ -10,7 +10,6 @@
 use Drupal\simpletest\WebTestBase;
 use Drupal\taxonomy\Entity\Term;
 use Drupal\taxonomy\Entity\Vocabulary;
-use Drupal\user\Entity\Role;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\Entity\Webform;
 
@@ -51,146 +50,6 @@ public function tearDown() {
     parent::tearDown();
   }
 
-  /****************************************************************************/
-  // User.
-  /****************************************************************************/
-
-  /**
-   * A normal user to submit webforms.
-   *
-   * @var \Drupal\user\UserInterface
-   */
-  protected $normalUser;
-
-  /**
-   * A webform administrator.
-   *
-   * @var \Drupal\user\UserInterface
-   */
-  protected $adminWebformUser;
-
-  /**
-   * A webform submission administrator.
-   *
-   * @var \Drupal\user\UserInterface
-   */
-  protected $adminSubmissionUser;
-
-  /**
-   * A webform own access.
-   *
-   * @var \Drupal\user\UserInterface
-   */
-  protected $ownWebformUser;
-
-  /**
-   * A webform any access.
-   *
-   * @var \Drupal\user\UserInterface
-   */
-  protected $anyWebformUser;
-
-  /**
-   * A webform submission own access.
-   *
-   * @var \Drupal\user\UserInterface
-   */
-  protected $ownWebformSubmissionUser;
-
-  /**
-   * A webform submission any access.
-   *
-   * @var \Drupal\user\UserInterface
-   */
-  protected $anyWebformSubmissionUser;
-
-  /**
-   * Create webform test users.
-   */
-  protected function createUsers() {
-    // Default user permissions.
-    $default_user_permissions = [];
-    $default_user_permissions[] = 'access user profiles';
-    if (in_array('webform_node', static::$modules)) {
-      $default_user_permissions[] = 'access content';
-    }
-
-    // Normal user.
-    $normal_user_permissions = $default_user_permissions;
-    $this->normalUser = $this->drupalCreateUser($normal_user_permissions);
-
-    // Admin webform user.
-    $admin_form_user_permissions = array_merge($default_user_permissions, [
-      'access site reports',
-      'administer site configuration',
-      'administer webform',
-      'access webform submission log',
-      'create webform',
-      'administer users',
-    ]);
-    if (in_array('block', static::$modules)) {
-      $admin_form_user_permissions[] = 'administer blocks';
-    }
-    if (in_array('webform_node', static::$modules)) {
-      $admin_form_user_permissions[] = 'administer nodes';
-    }
-    if (in_array('webform_test_translation', static::$modules)) {
-      $admin_form_user_permissions[] = 'translate configuration';
-    }
-    $this->adminWebformUser = $this->drupalCreateUser($admin_form_user_permissions);
-
-    // Own webform user.
-    $this->ownWebformUser = $this->drupalCreateUser(array_merge($default_user_permissions, [
-      'access webform overview',
-      'create webform',
-      'edit own webform',
-      'delete own webform',
-      'view own webform submission',
-      'edit own webform submission',
-      'delete own webform submission',
-    ]));
-
-    // Any webform user.
-    $this->anyWebformUser = $this->drupalCreateUser(array_merge($default_user_permissions, [
-      'access webform overview',
-      'create webform',
-      'edit any webform',
-      'delete any webform',
-    ]));
-
-    // Own webform submission user.
-    $this->ownWebformSubmissionUser = $this->drupalCreateUser(array_merge($default_user_permissions, [
-      'view own webform submission',
-      'edit own webform submission',
-      'delete own webform submission',
-    ]));
-
-    // Any webform submission user.
-    $this->anyWebformSubmissionUser = $this->drupalCreateUser(array_merge($default_user_permissions, [
-      'view any webform submission',
-      'edit any webform submission',
-      'delete any webform submission',
-    ]));
-
-    // Admin submission user.
-    $this->adminSubmissionUser = $this->drupalCreateUser(array_merge($default_user_permissions, [
-      'access webform submission log',
-      'administer webform submission',
-    ]));
-  }
-
-  /**
-   * Add webform submission own permissions to anonymous role.
-   */
-  protected function addWebformSubmissionOwnPermissionsToAnonymous() {
-    /** @var \Drupal\user\RoleInterface $anonymous_role */
-    $anonymous_role = Role::load('anonymous');
-    $anonymous_role->grantPermission('view own webform submission')
-      ->grantPermission('edit own webform submission')
-      ->grantPermission('delete own webform submission')
-      ->save();
-  }
-
   /****************************************************************************/
   // Block.
   /****************************************************************************/
@@ -365,77 +224,18 @@ protected function postSubmission(WebformInterface $webform, array $edit = [], $
    *   Submission values.
    * @param string $submit
    *   Value of the submit button whose click is to be emulated.
-   * @param $options
+   * @param array $options
    *   Options to be forwarded to the url generator.
    *
    * @return int
    *   The created test submission's sid.
    */
-  protected function postSubmissionTest(WebformInterface $webform, array $edit = [], $submit = NULL, $options = []) {
+  protected function postSubmissionTest(WebformInterface $webform, array $edit = [], $submit = NULL, array $options = []) {
     $submit = $this->getWebformSubmitButtonLabel($webform, $submit);
     $this->drupalPostForm('webform/' . $webform->id() . '/test', $edit, $submit, $options);
     return $this->getLastSubmissionId($webform);
   }
 
-  /****************************************************************************/
-  // Log.
-  /****************************************************************************/
-
-  /**
-   * Get the last submission id.
-   *
-   * @return int
-   *   The last submission id.
-   */
-  protected function getLastSubmissionLog() {
-    $query = \Drupal::database()->select('webform_submission_log', 'l');
-    $query->leftJoin('webform_submission', 'ws', 'l.sid = ws.sid');
-    $query->fields('l', [
-      'lid',
-      'uid',
-      'sid',
-      'handler_id',
-      'operation',
-      'message',
-      'timestamp',
-    ]);
-    $query->fields('ws', [
-      'webform_id',
-      'entity_type',
-      'entity_id',
-    ]);
-    $query->orderBy('l.lid', 'DESC');
-    $query->range(0, 1);
-    return $query->execute()->fetch();
-  }
-
-  /**
-   * Get the entire submission log.
-   *
-   * @return int
-   *   The last submission id.
-   */
-  protected function getSubmissionLog() {
-    $query = \Drupal::database()->select('webform_submission_log', 'l');
-    $query->leftJoin('webform_submission', 'ws', 'l.sid = ws.sid');
-    $query->fields('l', [
-      'lid',
-      'uid',
-      'sid',
-      'handler_id',
-      'operation',
-      'message',
-      'timestamp',
-    ]);
-    $query->fields('ws', [
-      'webform_id',
-      'entity_type',
-      'entity_id',
-    ]);
-    $query->orderBy('l.lid', 'DESC');
-    return $query->execute()->fetchAll();
-  }
-
   /****************************************************************************/
   // Export.
   /****************************************************************************/
@@ -452,7 +252,7 @@ protected function getExport(WebformInterface $webform, array $options = []) {
     /** @var \Drupal\webform\WebformSubmissionExporterInterface $exporter */
     $exporter = \Drupal::service('webform_submission.exporter');
     $options += $exporter->getDefaultExportOptions();
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/download', ['query' => $options]);
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/download', ['query' => $options]);
   }
 
   /**
@@ -499,7 +299,7 @@ protected function getLastEmail() {
   /**
    * Passes if the substring is contained within text, fails otherwise.
    */
-  protected function assertContains($haystack, $needle, $message = '', $group = 'Other') {
+  protected function assertContains($needle, $haystack, $message = '') {
     if (!$message) {
       $t_args = [
         '@haystack' => Unicode::truncate($haystack, 150, TRUE, TRUE),
@@ -511,13 +311,13 @@ protected function assertContains($haystack, $needle, $message = '', $group = 'O
     if (!$result) {
       $this->verbose($haystack);
     }
-    return $this->assert($result, $message, $group);
+    $this->assert($result, $message);
   }
 
   /**
    * Passes if the substring is not contained within text, fails otherwise.
    */
-  protected function assertNotContains($haystack, $needle, $message = '', $group = 'Other') {
+  protected function assertNotContains($needle, $haystack, $message = '') {
     if (!$message) {
       $t_args = [
         '@haystack' => Unicode::truncate($haystack, 150, TRUE, TRUE),
@@ -530,7 +330,7 @@ protected function assertNotContains($haystack, $needle, $message = '', $group =
     if (!$result) {
       $this->verbose($haystack);
     }
-    return $this->assert($result, $message, $group);
+    $this->assert($result, $message);
   }
 
   /**
diff --git a/web/modules/webform/src/Tests/WebformTestTrait.php b/web/modules/webform/src/Tests/WebformTestTrait.php
index fed6e694f1358bceba39534a35f1627b15876ebb..4697391a49878e84bfbf13983078b981daea831a 100644
--- a/web/modules/webform/src/Tests/WebformTestTrait.php
+++ b/web/modules/webform/src/Tests/WebformTestTrait.php
@@ -161,6 +161,7 @@ protected function getLastSubmissionId(WebformInterface $webform) {
       $entity_ids = \Drupal::entityQuery('webform_submission')
         ->sort('sid', 'DESC')
         ->condition('webform_id', $webform->id())
+        ->accessCheck(FALSE)
         ->execute();
       return reset($entity_ids);
     }
diff --git a/web/modules/webform/src/Tests/WebformThirdPartySettingsTest.php b/web/modules/webform/src/Tests/WebformThirdPartySettingsTest.php
index 0027551d1ea72e4600c59ae474c63b93e03d7fbe..0c6d3a67c6d7609453950d17d7e453e6f864a643 100644
--- a/web/modules/webform/src/Tests/WebformThirdPartySettingsTest.php
+++ b/web/modules/webform/src/Tests/WebformThirdPartySettingsTest.php
@@ -23,22 +23,22 @@ public function testThirdPartySettings() {
     $this->drupalLogin($this->rootUser);
 
     // Check 'Webform: Settings' shows no modules installed.
-    $this->drupalGet('admin/structure/webform/config');
+    $this->drupalGet('/admin/structure/webform/config');
     $this->assertRaw('There are no third party settings available.');
 
     // Check 'Contact: Settings' does not show 'Third party settings'.
-    $this->drupalGet('admin/structure/webform/manage/contact/settings');
+    $this->drupalGet('/admin/structure/webform/manage/contact/settings');
     $this->assertNoRaw('Third party settings');
 
     // Install test third party settings module.
     \Drupal::service('module_installer')->install(['webform_test_third_party_settings']);
 
     // Check 'Webform: Settings' shows no modules installed.
-    $this->drupalGet('admin/structure/webform/config');
+    $this->drupalGet('/admin/structure/webform/config');
     $this->assertNoRaw('There are no third party settings available.');
 
     // Check 'Contact: Settings' shows 'Third party settings'.
-    $this->drupalGet('admin/structure/webform/manage/contact/settings');
+    $this->drupalGet('/admin/structure/webform/manage/contact/settings');
     $this->assertRaw('Third party settings');
 
     // Check 'Webform: Settings' message.
@@ -46,7 +46,7 @@ public function testThirdPartySettings() {
       'third_party_settings[webform_test_third_party_settings][message]' => 'Message for all webforms',
     ];
     $this->drupalPostForm('admin/structure/webform/config', $edit, t('Save configuration'));
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertRaw('Message for all webforms');
 
     // Check that webform.settings.yml contain message.
@@ -60,12 +60,12 @@ public function testThirdPartySettings() {
       'third_party_settings[webform_test_third_party_settings][message]' => 'Message for only this webform',
     ];
     $this->drupalPostForm('admin/structure/webform/manage/contact/settings', $edit, t('Save'));
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertRaw('Message for only this webform');
 
     // Uninstall test third party settings module.
     \Drupal::service('module_installer')->uninstall(['webform_test_third_party_settings']);
-    $this->drupalGet('webform/contact');
+    $this->drupalGet('/webform/contact');
     $this->assertNoRaw('Message for only this webform');
 
     // Check that webform.settings.yml no longer contains message or
diff --git a/web/modules/webform/src/Tests/WebformTokenSubmissionValueTest.php b/web/modules/webform/src/Tests/WebformTokenSubmissionValueTest.php
index dd9daa6096ded68f827beb991c2a8a3984de50cf..51f55ef4dd58c49af533c48a94717d8837009656 100644
--- a/web/modules/webform/src/Tests/WebformTokenSubmissionValueTest.php
+++ b/web/modules/webform/src/Tests/WebformTokenSubmissionValueTest.php
@@ -40,15 +40,12 @@ public function setUp() {
    */
   public function testWebformTokenSubmissionValue() {
     $webform = Webform::load('test_token_submission_value');
-    $this->postSubmission($webform);
 
+    // Check anonymous token handling.
+    $this->postSubmission($webform);
     $tokens = [
-
       // Emails.
       'webform_submission:values:email' => 'example@example.com',
-      'webform_submission:values:emails' => '- one@example.com
-- two@example.com
-- three@example.com',
       'webform_submission:values:emails:0' => 'one@example.com',
       'webform_submission:values:emails:1' => 'two@example.com',
       'webform_submission:values:emails:2' => 'three@example.com',
@@ -66,6 +63,10 @@ public function testWebformTokenSubmissionValue() {
       'webform_submission:values:users:0:entity:account-name' => 'admin',
       'webform_submission:values:users:99:entity:account-name' => '',
 
+      // Current users.
+      'current-user:display-name' => '',
+      'current-user:missing' => '',
+
       // Terms.
       'webform_submission:values:term' => 'Parent 1 (1)',
       'webform_submission:values:terms' => 'Parent 1 (1), Parent 1: Child 1 (2)',
@@ -104,6 +105,13 @@ public function testWebformTokenSubmissionValue() {
       'webform_submission:values:contacts:0:email:html' => '<a href="mailto:john@example.com">john@example.com</a>',
       'webform_submission:values:contacts:1:email:raw:html' => 'jane@example.com',
 
+      // Containers.
+      'webform_submission:values:fieldset' => '<pre>fieldset
+--------
+first_name: John
+last_name: Smith
+</pre>',
+
       // Submission limits.
       'webform_submission:limit:webform' => '100',
       'webform_submission:total:webform' => '1',
@@ -117,17 +125,42 @@ public function testWebformTokenSubmissionValue() {
       // Clear.
       'webform_submission:values:missing' => '[webform_submission:values:missing]',
       'webform_submission:values:missing:clear' => '',
+      'webform:random:missing' => '[webform:random:missing]',
+      'webform:random:missing:clear' => '',
+
+      // HTML decode.
+      'webform_submission:values:markup' => '&lt;b&gt;Bold&lt;/b&gt; &amp;amp; UPPERCASE',
+      'webform_submission:values:markup:htmldecode' => '<b>Bold</b> &amp; UPPERCASE',
+      'webform_submission:values:markup:htmldecode:striptags' => 'Bold &amp; UPPERCASE',
+      'webform_submission:values:script' => '&lt;script&gt;alert(&#039;hi&#039;);&lt;/script&gt;',
+      'webform_submission:values:script:htmldecode' => 'alert(&#039;hi&#039;);',
+
+      // URL encode.
+      'webform_submission:values:url' => 'http://example.com?query=param',
+      'webform_submission:values:url:urlencode' => 'http%3A%2F%2Fexample.com%3Fquery%3Dparam',
     ];
     foreach ($tokens as $token => $value) {
       $this->assertRaw("<tr><th width=\"50%\">$token</th><td width=\"50%\">$value</td></tr>");
     }
 
-    // Containers.
+    // Check containers.
     $this->assertRaw('<tr><th width="50%">webform_submission:values:fieldset</th><td width="50%"><pre>fieldset');
     $this->assertRaw('<tr><th width="50%">webform_submission:values:fieldset:html</th><td width="50%"><fieldset class="webform-container webform-container-type-fieldset js-form-item form-item js-form-wrapper form-wrapper" id="test_token_submission_value--fieldset">');
-    $this->assertRaw('<tr><th width="50%">webform_submission:values:fieldset:header:html</th><td width="50%"><section id="test_token_submission_value--fieldset" class="js-form-item form-item js-form-wrapper form-wrapper webform-section">');
-    $this->assertRaw('<tr><th width="50%">webform_submission:values:fieldset:details:html</th><td width="50%"><details data-webform-element-id="test_token_submission_value--fieldset" class="webform-container webform-container-type-details js-form-wrapper form-wrapper" id="test_token_submission_value--fieldset" open="open">');
+    $this->assertRaw('<tr><th width="50%">webform_submission:values:fieldset:header:html</th><td width="50%"><section class="js-form-item form-item js-form-wrapper form-wrapper webform-section" id="test_token_submission_value--fieldset">');
+    $this->assertRaw('<tr><th width="50%">webform_submission:values:fieldset:details:html</th><td width="50%"><details class="webform-container webform-container-type-details js-form-wrapper form-wrapper" data-webform-element-id="test_token_submission_value--fieldset" id="test_token_submission_value--fieldset" open="open">');
     $this->assertRaw('<tr><th width="50%">webform_submission:values:fieldset:fieldset:html</th><td width="50%"><fieldset class="webform-container webform-container-type-fieldset js-form-item form-item js-form-wrapper form-wrapper" id="test_token_submission_value--fieldset">');
+
+    // Check authenticated token handling.
+    $this->drupalLogin($this->rootUser);
+    $this->postSubmission($webform);
+    $tokens = [
+      // Current users.
+      'current-user:display-name' => 'admin',
+      'current-user:missing' => '',
+    ];
+    foreach ($tokens as $token => $value) {
+      $this->assertRaw("<tr><th width=\"50%\">$token</th><td width=\"50%\">$value</td></tr>");
+    }
   }
 
 }
diff --git a/web/modules/webform/src/Tests/WebformTokenSuffixesTest.php b/web/modules/webform/src/Tests/WebformTokenSuffixesTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..16120a489c692c52a29058834fe54d8444cab970
--- /dev/null
+++ b/web/modules/webform/src/Tests/WebformTokenSuffixesTest.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\webform\Tests;
+
+/**
+ * Tests for webform token suffixes.
+ *
+ * @group Webform
+ */
+class WebformTokenSuffixesTest extends WebformTestBase {
+
+  /**
+   * Test webform token suffixes.
+   */
+  public function testTokenSuffixes() {
+    /** @var \Drupal\webform\WebformTokenManagerInterface $token_manager */
+    $token_manager = \Drupal::service('webform.token_manager');
+
+    $tests = [
+      // Default.
+      [
+        'site_name' => 'Testing',
+        'text' => '[site:name]',
+        'expected' => 'Testing',
+        'message' => 'Basic token',
+      ],
+      // :clear.
+      [
+        'text' => '[missing]',
+        'expected' => '[missing]',
+        'message' => 'Missing token',
+      ],
+      [
+        'text' => '[missing:clear]',
+        'expected' => '',
+        'message' => 'Missing token cleared',
+      ],
+      [
+        'text' => '[missing:clear]',
+        'expected' => '[missing:clear]',
+        'message' => 'Clear disabled',
+        'options' => ['suffixes' => ['clear' => FALSE]],
+      ],
+      // :htmldecode.
+      [
+        'site_name' => '<b>Testing</b>',
+        'text' => '[site:name]',
+        'expected' => '&lt;b&gt;Testing&lt;/b&gt;',
+        'message' => 'Basic token with encoded HTML markup',
+      ],
+      [
+        'site_name' => '<b>Testing</b>',
+        'text' => '[site:name:htmldecode]',
+        'expected' => '<b>Testing</b>',
+        'message' => 'Basic token with decoded HTML markup',
+      ],
+      // :striptags.
+      [
+        'site_name' => '<b>Testing</b>',
+        'text' => '[site:name:htmldecode:striptags]',
+        'expected' => 'Testing',
+        'message' => 'Basic token with decoded HTML markup',
+        'options' => [],
+      ],
+      // :xmlencode.
+      [
+        'site_name' => '<b>Testing</b>',
+        'text' => '[site:name:xmlencode]',
+        'expected' => '&amp;lt;b&amp;gt;Testing&amp;lt;/b&amp;gt;',
+        'message' => 'XML encode',
+      ],
+      [
+        'site_name' => '<b>Testing</b>',
+        'text' => '[site:name:htmldecode:xmlencode]',
+        'expected' => '&lt;b&gt;Testing&lt;/b&gt;',
+        'message' => 'HTML decode and then XML encode',
+      ],
+    ];
+    foreach ($tests as $test) {
+      // Set default options.
+      $test += [
+        'options' => [],
+      ];
+
+      // Set site name.
+      if (!empty($test['site_name'])) {
+        \Drupal::configFactory()
+          ->getEditable('system.site')
+          ->set('name', $test['site_name'])
+          ->save();
+      }
+      $result = $token_manager->replace($test['text'], NULL, [], $test['options']);
+      $this->assertEqual($result, $test['expected'], $test['message']);
+    }
+
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Wizard/WebformWizardAccessTest.php b/web/modules/webform/src/Tests/Wizard/WebformWizardAccessTest.php
index a76a2c69bb9a386536ece7c02b5b96851219ed3c..82a371de893027dc146e3f4aa93956b4098aa6d8 100644
--- a/web/modules/webform/src/Tests/Wizard/WebformWizardAccessTest.php
+++ b/web/modules/webform/src/Tests/Wizard/WebformWizardAccessTest.php
@@ -25,7 +25,7 @@ public function testConditionalWizard() {
     $webform = Webform::load('test_form_wizard_access');
 
     // Check anonymous user can access 'All' and 'Anonymous' form pages.
-    $this->drupalGet('webform/test_form_wizard_access');
+    $this->drupalGet('/webform/test_form_wizard_access');
     $this->assertRaw('<b>All</b>');
     $this->assertRaw('<b>Anonymous</b>');
     $this->assertNoRaw('<b>Authenticated</b>');
@@ -54,7 +54,7 @@ public function testConditionalWizard() {
     $this->drupalLogin($this->rootUser);
 
     // Check authenticated user can access 'All', 'Authenticated', and 'Private' form pages.
-    $this->drupalGet('webform/test_form_wizard_access');
+    $this->drupalGet('/webform/test_form_wizard_access');
     $this->assertRaw('<b>All</b>');
     $this->assertNoRaw('<b>Anonymous</b>');
     $this->assertRaw('<b>Authenticated</b>');
diff --git a/web/modules/webform/src/Tests/Wizard/WebformWizardAdvancedTest.php b/web/modules/webform/src/Tests/Wizard/WebformWizardAdvancedTest.php
index 8acada5e20d1bdbd34af393bacbc6d0ce2a18684..cf54fa8607d0dd9359f50d3391fcf2cf1b61b903 100644
--- a/web/modules/webform/src/Tests/Wizard/WebformWizardAdvancedTest.php
+++ b/web/modules/webform/src/Tests/Wizard/WebformWizardAdvancedTest.php
@@ -26,7 +26,7 @@ public function testAdvancedWizard() {
     $webform = Webform::load('test_form_wizard_advanced');
 
     // Get initial wizard start page (Your Information).
-    $this->drupalGet('webform/test_form_wizard_advanced');
+    $this->drupalGet('/webform/test_form_wizard_advanced');
     // Check progress bar is set to 'Your Information'.
     $this->assertPattern('#<li data-webform-page="information" class="webform-progress-bar__page webform-progress-bar__page--current">\s+<b>Your Information</b><span></span></li>#');
     // Check progress pages.
@@ -120,7 +120,7 @@ public function testAdvancedWizard() {
     ];
     $this->drupalPostForm(NULL, $edit, t('Save Draft'));
     // Complete reload the webform.
-    $this->drupalGet('webform/test_form_wizard_advanced');
+    $this->drupalGet('/webform/test_form_wizard_advanced');
     // Check progress bar is still set to 'Contact Information'.
     $this->assertCurrentPage('Contact Information', 'contact');
 
@@ -158,7 +158,7 @@ public function testAdvancedWizard() {
     $this->assertRaw('<a href="mailto:janesmith@example.com">janesmith@example.com</a>');
     $this->assertRaw('<label>Phone</label>');
     $this->assertRaw('<a href="tel:111-111-1111">111-111-1111</a>');
-    $this->assertRaw('<div id="test_form_wizard_advanced--comments" class="webform-element webform-element-type-textarea js-form-item form-item js-form-type-item form-type-item js-form-item-comments form-item-comments form-no-label">');
+    $this->assertRaw('<div class="webform-element webform-element-type-textarea js-form-item form-item js-form-type-item form-type-item js-form-item-comments form-item-comments form-no-label" id="test_form_wizard_advanced--comments">');
     $this->assertRaw('This is working fine.');
 
     // Submit the webform.
@@ -225,7 +225,7 @@ public function testAdvancedWizard() {
       'wizard_progress_percentage' => TRUE,
     ] + $webform->getSettings());
     $webform->save();
-    $this->drupalGet('webform/test_form_wizard_advanced');
+    $this->drupalGet('/webform/test_form_wizard_advanced');
 
     // Check no progress bar.
     $this->assertNoRaw('class="webform-progress-bar"');
@@ -242,7 +242,7 @@ public function testAdvancedWizard() {
     \Drupal::configFactory()->getEditable('webform.settings')
       ->set('settings.default_wizard_confirmation_label', '{global complete}')
       ->save();
-    $this->drupalGet('webform/test_form_wizard_advanced');
+    $this->drupalGet('/webform/test_form_wizard_advanced');
     $this->assertRaw('{global complete}');
 
     // Check webform complete label.
@@ -251,7 +251,7 @@ public function testAdvancedWizard() {
       'wizard_confirmation_label' => '{webform complete}',
     ] + $webform->getSettings());
     $webform->save();
-    $this->drupalGet('webform/test_form_wizard_advanced');
+    $this->drupalGet('/webform/test_form_wizard_advanced');
     $this->assertRaw('{webform complete}');
 
     // Check webform exclude complete.
@@ -259,13 +259,13 @@ public function testAdvancedWizard() {
       'wizard_confirmation' => FALSE,
     ] + $webform->getSettings());
     $webform->save();
-    $this->drupalGet('webform/test_form_wizard_advanced');
+    $this->drupalGet('/webform/test_form_wizard_advanced');
 
     // Check complete label.
     $this->assertRaw('class="webform-progress-bar"');
     // Check complete is missing from confirmation page.
     $this->assertNoRaw('{webform complete}');
-    $this->drupalGet('webform/test_form_wizard_advanced/confirmation');
+    $this->drupalGet('/webform/test_form_wizard_advanced/confirmation');
     $this->assertNoRaw('class="webform-progress-bar"');
   }
 
diff --git a/web/modules/webform/src/Tests/Wizard/WebformWizardBasicTest.php b/web/modules/webform/src/Tests/Wizard/WebformWizardBasicTest.php
index 159edc71c3f3c1713dc4ed94cb524c79fa06be0d..886e55a552dbb49aa177c7f9a8a5032a132cbe50 100644
--- a/web/modules/webform/src/Tests/Wizard/WebformWizardBasicTest.php
+++ b/web/modules/webform/src/Tests/Wizard/WebformWizardBasicTest.php
@@ -19,7 +19,7 @@ class WebformWizardBasicTest extends WebformWizardTestBase {
   protected static $testWebforms = ['test_form_wizard_basic'];
 
   /**
-   * Test webform advanced wizard.
+   * Test webform basic wizard.
    */
   public function testBasicWizard() {
     $this->drupalLogin($this->rootUser);
@@ -53,7 +53,7 @@ public function testBasicWizard() {
       ->save();
 
     // Check next page.
-    $this->drupalGet('webform/test_form_wizard_basic');
+    $this->drupalGet('/webform/test_form_wizard_basic');
     $this->assertNoRaw('data-webform-wizard-current-page');
     $this->assertRaw('data-webform-wizard-page="page_2" data-drupal-selector="edit-wizard-next"');
 
@@ -75,7 +75,7 @@ public function testBasicWizard() {
     $wizard_webform->setSetting('wizard_track', 'index')->save();
 
     // Check next page.
-    $this->drupalGet('webform/test_form_wizard_basic');
+    $this->drupalGet('/webform/test_form_wizard_basic');
     $this->assertRaw('data-webform-wizard-page="2" data-drupal-selector="edit-wizard-next"');
 
     // Check next and previous page.
diff --git a/web/modules/webform/src/Tests/Wizard/WebformWizardConditionalTest.php b/web/modules/webform/src/Tests/Wizard/WebformWizardConditionalTest.php
index 8541da6024e3ecbba63173a2760b1bbe07a19720..15418b3fc8c44c0d7ab9f9c97cca8e7234e2de05 100644
--- a/web/modules/webform/src/Tests/Wizard/WebformWizardConditionalTest.php
+++ b/web/modules/webform/src/Tests/Wizard/WebformWizardConditionalTest.php
@@ -23,7 +23,7 @@ class WebformWizardConditionalTest extends WebformWizardTestBase {
    */
   public function testConditionalWizard() {
     $webform = Webform::load('test_form_wizard_conditional');
-    $this->drupalGet('webform/test_form_wizard_conditional');
+    $this->drupalGet('/webform/test_form_wizard_conditional');
 
     // Check hiding page 1, 3, and 5.
     $edit = [
diff --git a/web/modules/webform/src/Tests/Wizard/WebformWizardCustomTest.php b/web/modules/webform/src/Tests/Wizard/WebformWizardCustomTest.php
index ef60554e60b7fa5f27ce5312c8d51bc959429a7e..45000eea15abfc77d39d5e4e270f6081221de592 100644
--- a/web/modules/webform/src/Tests/Wizard/WebformWizardCustomTest.php
+++ b/web/modules/webform/src/Tests/Wizard/WebformWizardCustomTest.php
@@ -28,7 +28,7 @@ class WebformWizardCustomTest extends WebformWizardTestBase {
    */
   public function testCustomWizard() {
     // Check current page is #1.
-    $this->drupalGet('webform/test_form_wizard_custom');
+    $this->drupalGet('/webform/test_form_wizard_custom');
     $this->assertCurrentPage('Wizard page #1', 'wizard_1');
 
     // Check next page is #2.
diff --git a/web/modules/webform/src/Tests/Wizard/WebformWizardLinksTest.php b/web/modules/webform/src/Tests/Wizard/WebformWizardLinksTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..642d900bff77a8b0423cd19e05c5c61fcfa88042
--- /dev/null
+++ b/web/modules/webform/src/Tests/Wizard/WebformWizardLinksTest.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\webform\Tests\Wizard;
+
+use Drupal\webform\Entity\Webform;
+
+/**
+ * Tests for webform wizard progress and preview links.
+ *
+ * @group Webform
+ */
+class WebformWizardLinksTest extends WebformWizardTestBase {
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_form_wizard_links'];
+
+  /**
+   * Test webform wizard progress and preview links.
+   */
+  public function testWizardLinks() {
+    $this->drupalLogin($this->rootUser);
+
+    $wizard_webform = Webform::load('test_form_wizard_links');
+
+    // Check that first page has no links.
+    $this->drupalGet('/webform/test_form_wizard_links');
+    $this->assertCssSelect('.webform-wizard-pages-links');
+    $this->assertNoFieldByName('webform_wizard_page-page_1', t('Edit'));
+    $this->assertNoFieldByName('webform_wizard_page-page_2', t('Edit'));
+
+    // Check that second page links to first page.
+    $this->drupalPostForm('webform/test_form_wizard_links', [], t('Next Page >'));
+    $this->assertCssSelect('.webform-wizard-pages-links');
+    $this->assertFieldByName('webform_wizard_page-page_1', t('Edit'));
+    $this->assertNoFieldByName('webform_wizard_page-page_2', t('Edit'));
+
+    // Check that preview links to first and second page.
+    $this->drupalPostForm('webform/test_form_wizard_links', [], t('Preview'));
+    $this->assertCssSelect('.webform-wizard-pages-links');
+    $this->assertFieldByName('webform_wizard_page-page_1', t('Edit'));
+    $this->assertFieldByName('webform_wizard_page-page_2', t('Edit'));
+
+    // Check that preview links are not wrapper in .form-actions.
+    $this->assertNoCssSelect('.webform-wizard-pages-links.form-actions');
+
+    // Check 'wizard_progress_link' setting.
+    $this->assertCssSelect('.webform-wizard-pages-links[data-wizard-progress-link="true"]');
+
+    // Check 'wizard_preview_link' setting.
+    $this->assertCssSelect('.webform-wizard-pages-links[data-wizard-preview-link="true"]');
+
+    // Set preview links to FALSE.
+    $wizard_webform->setSetting('wizard_preview_link', FALSE)->save();
+
+    // Check preview page is not linked.
+    $this->drupalGet('/webform/test_form_wizard_links');
+    $this->assertCssSelect('.webform-wizard-pages-links[data-wizard-progress-link="true"]');
+    $this->assertNoCssSelect('.webform-wizard-pages-links[data-wizard-preview-link="true"]');
+
+    // Set progress bar links to FALSE.
+    $wizard_webform
+      ->setSetting('wizard_progress_link', FALSE)
+      ->setSetting('wizard_preview_link', TRUE)
+      ->save();
+
+    // Check progress bar is not linked.
+    $this->drupalGet('/webform/test_form_wizard_links');
+    $this->assertNoCssSelect('.webform-wizard-pages-links[data-wizard-progress-link="true"]');
+    $this->assertCssSelect('.webform-wizard-pages-links[data-wizard-preview-link="true"]');
+
+    // Set progress bar links and preview page to FALSE.
+    $wizard_webform
+      ->setSetting('wizard_progress_link', FALSE)
+      ->setSetting('wizard_preview_link', FALSE)
+      ->save();
+
+    $this->drupalGet('/webform/test_form_wizard_links');
+    $this->assertNoCssSelect('.webform-wizard-pages-links');
+  }
+
+}
diff --git a/web/modules/webform/src/Tests/Wizard/WebformWizardValidateTest.php b/web/modules/webform/src/Tests/Wizard/WebformWizardValidateTest.php
index da39a0a30d585f59672253d52418f233e713ef7c..53723b40487d119f3c5a4210d57e3a568b293c12 100644
--- a/web/modules/webform/src/Tests/Wizard/WebformWizardValidateTest.php
+++ b/web/modules/webform/src/Tests/Wizard/WebformWizardValidateTest.php
@@ -27,7 +27,7 @@ class WebformWizardValidateTest extends WebformWizardTestBase {
    * Test webform wizard validation.
    */
   public function testWizardValidate() {
-    $this->drupalGet('webform/test_form_wizard_validate');
+    $this->drupalGet('/webform/test_form_wizard_validate');
 
     /**************************************************************************/
     // Basic validation.
@@ -255,5 +255,4 @@ public function testWizardValidate() {
     $this->assertRaw($raw);
   }
 
-
 }
diff --git a/web/modules/webform/src/Twig/TwigExtension.php b/web/modules/webform/src/Twig/TwigExtension.php
index 1c17fe4e49ad9df1cc176e0692a71879561f74fd..020c873414aba80f7019560417715363bbd71c29 100644
--- a/web/modules/webform/src/Twig/TwigExtension.php
+++ b/web/modules/webform/src/Twig/TwigExtension.php
@@ -52,12 +52,25 @@ public function getName() {
    * @see \Drupal\Core\Utility\Token::replace
    */
   public function webformToken($token, EntityInterface $entity = NULL, array $data = [], array $options = []) {
+    static $processing = [];
+
     // Allow the webform_token function to be tested during validation without
     // a valid entity.
     if (!$entity) {
       return $token;
     }
 
+    // Prevent token replacement recursion.
+    $original_token = $token;
+    $processing += [$original_token => 0];
+    $processing[$original_token]++;
+    if ($processing[$original_token] > 100) {
+      // Cancel token processing by settings the processing token to FALSE.
+      $processing[$original_token] = FALSE;
+      // Throw exception which is caught by ::renderTwigTemplate.
+      throw new \LogicException(sprintf('The "%s" token is being called recursively.', $token));
+    }
+
     // Parse options included in the token.
     // @see \Drupal\webform\Twig\TwigExtension::renderTwigTemplate
     foreach (static::$options as $option_name => $option_setting) {
@@ -72,8 +85,16 @@ public function webformToken($token, EntityInterface $entity = NULL, array $data
     // ISSUE. This TwigExtension is loaded on every page load, even when a
     // website is in maintenance mode.
     // @see https://www.drupal.org/node/2907960
-    /** @var \Drupal\webform\WebformTokenManagerInterface $value */
-    $value = \Drupal::service('webform.token_manager')->replace($token, $entity, $data, $options);
+    /** @var \Drupal\webform\WebformTokenManagerInterface $token_manager */
+    $token_manager = \Drupal::service('webform.token_manager');
+    $value = $token_manager->replace($token, $entity, $data, $options);
+
+    // If token replacement caused a recursion exception return an empty value.
+    if ($processing[$original_token] === FALSE) {
+      return '';
+    }
+
+    $processing[$original_token]--;
 
     return (WebformHtmlHelper::containsHtml($value)) ? ['#markup' => $value] : $value;
   }
@@ -88,27 +109,34 @@ public function webformToken($token, EntityInterface $entity = NULL, array $data
   /**
    * Build reusable Twig help.
    *
+   * @param array $variables
+   *   An array of available variable names.
+   *
    * @return array
    *   A renderable array container Twig help.
    */
-  public static function buildTwigHelp() {
+  public static function buildTwigHelp(array $variables = []) {
     /** @var \Drupal\webform\WebformSubmissionStorageInterface $submission_storage */
     $submission_storage = \Drupal::entityTypeManager()->getStorage('webform_submission');
     $field_definitions = $submission_storage->getFieldDefinitions();
-    $items = [
+
+    // Bold all the passed variable names.
+    foreach ($variables as $index => $item) {
+      $variables[$index] = ['#markup' => $item, '#prefix' => '<strong>', '#suffix' => '</strong>'];
+    }
+
+    $variables = array_merge($variables, [
       '{{ webform }}',
       '{{ webform_submission }}',
       '{{ elements }}',
       '{{ elements_flattened }}',
-      // @todo Dynamically generate examples for all elements.
-      // This could be overkill.
       '{{ data.element_key }}',
       '{{ data.element_key.delta }}',
       '{{ data.composite_element_key.subelement_key }}',
       '{{ data.composite_element_key.delta.subelement_key }}',
-    ];
+    ]);
     foreach (array_keys($field_definitions) as $field_name) {
-      $items[] = "{{ $field_name }}";
+      $variables[] = "{{ $field_name }}";
     }
 
     $t_args = [
@@ -124,13 +152,13 @@ public static function buildTwigHelp() {
     ];
     $output[] = [
       '#theme' => 'item_list',
-      '#items' => $items,
+      '#items' => $variables,
     ];
     $output[] = [
       '#markup' => '<p>' . t("You can also output tokens using the <code>webform_token()</code> function.") . '</p>',
     ];
     $output[] = [
-      '#markup' => "<pre>{{ webform_token('[webform_submission:values:element_value]', webform_submission) }}</pre>",
+      '#markup' => "<pre>{{ webform_token('[webform_submission:values:element_value]', webform_submission, [], options) }}</pre>",
     ];
     if (\Drupal::currentUser()->hasPermission('administer modules') && !\Drupal::moduleHandler()->moduleExists('twig_tweak')) {
       $t_args = [
@@ -160,7 +188,9 @@ public static function buildTwigHelp() {
    * @param string $template
    *   A inline Twig template.
    * @param array $options
-   *   Template and token options.
+   *   (optional) Template and token options.
+   * @param array $context
+   *   (optional) Context to be passed to inline Twig template.
    *
    * @return string
    *   The fully rendered Twig template.
@@ -168,7 +198,36 @@ public static function buildTwigHelp() {
    * @see \Drupal\webform\Element\WebformComputedTwig::processValue
    * @see \Drupal\webform\Plugin\WebformHandler\EmailWebformHandler::getMessage
    */
-  public static function renderTwigTemplate(WebformSubmissionInterface $webform_submission, $template, array $options) {
+  public static function renderTwigTemplate(WebformSubmissionInterface $webform_submission, $template, array $options = [], array $context = []) {
+    try {
+      $build = self::buildTwigTemplate($webform_submission, $template, $options, $context);
+      return \Drupal::service('renderer')->renderPlain($build);
+    }
+    catch (\Exception $exception) {
+      if ($webform_submission->getWebform()->access('update')) {
+        \Drupal::messenger()->addError($exception->getMessage());
+      }
+      \Drupal::logger('webform')->error($exception->getMessage());
+      return '';
+    }
+  }
+
+  /**
+   * Build a Twig template with a webform submission.
+   *
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   * @param string $template
+   *   A inline Twig template.
+   * @param array $options
+   *   (optional) Template, token options, and context.
+   * @param array $context
+   *   (optional) Context to be passed to inline Twig template.
+   *
+   * @return array
+   *   A renderable containing an inline twig template.
+   */
+  public static function buildTwigTemplate(WebformSubmissionInterface $webform_submission, $template, array $options = [], array $context = []) {
     $options += [
       'html' => FALSE,
       'email' => FALSE,
@@ -182,35 +241,34 @@ public static function renderTwigTemplate(WebformSubmissionInterface $webform_su
       }
     }
 
-    $context = [
+    // If the template does NOT use the webform_token() function, but contains
+    // simple tokens, convert the simple tokens to use
+    // the webform_token() function.
+    if (strpos($template, 'webform_token(') === FALSE
+      && strpos($template, '[webform') !== FALSE) {
+      $template = preg_replace('#([^"\']|^)(\[[^]]+\])([^"\']|$)#', '\1{{ webform_token(\'\2\', webform_submission) }}\3', $template);
+    }
+
+    $context += [
       'webform_submission' => $webform_submission,
       'webform' => $webform_submission->getWebform(),
       'elements' => $webform_submission->getWebform()->getElementsDecoded(),
       'elements_flattened' => $webform_submission->getWebform()->getElementsDecodedAndFlattened(),
+      'options' => $options,
     ] + $webform_submission->toArray(TRUE);
 
-    $build = [
+    return [
       '#type' => 'inline_template',
       '#template' => $template,
       '#context' => $context,
     ];
-
-    try {
-      return \Drupal::service('renderer')->renderPlain($build);
-    }
-    catch (\Exception $exception) {
-      if ($webform_submission->getWebform()->access('update')) {
-        drupal_set_message(t('Failed to render computed Twig value due to error "%error"', ['%error' => $exception->getMessage()]), 'error');
-      }
-      return '';
-    }
   }
 
   /**
-   * Determine if the  current user can edit Twig templates.
+   * Determine if the current user can edit Twig templates.
    *
    * @return bool
-   *   TRUE if the  current user can edit Twig templates.
+   *   TRUE if the current user can edit Twig templates.
    */
   public static function hasEditTwigAccess() {
     return (\Drupal::currentUser()->hasPermission('edit webform twig') || \Drupal::currentUser()->hasPermission('administer webform'));
diff --git a/web/modules/webform/src/Utility/Mail.php b/web/modules/webform/src/Utility/Mail.php
new file mode 100644
index 0000000000000000000000000000000000000000..3ed9a4778d7aa9acd02365311f6680adfa78e624
--- /dev/null
+++ b/web/modules/webform/src/Utility/Mail.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\webform\Utility;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\Unicode;
+
+/**
+ * Provides helpers to ensure emails are compliant with RFCs.
+ *
+ * @todo Remove this utility class once only Drupal 8.7.x is supported.
+ * @see https://www.drupal.org/node/3015116
+ */
+class Mail {
+
+  /**
+   * RFC-2822 "specials" characters.
+   */
+  const RFC_2822_SPECIALS = '()<>[]:;@\,."';
+
+  /**
+   * Return a RFC-2822 compliant "display-name" component.
+   *
+   * The "display-name" component is used in mail header "Originator" fields
+   * (From, Sender, Reply-to) to give a human-friendly description of the
+   * address, i.e. From: My Display Name <xyz@example.org>. RFC-822 and
+   * RFC-2822 define its syntax and rules. This method gets as input a string
+   * to be used as "display-name" and formats it to be RFC compliant.
+   *
+   * @param string $string
+   *   A string to be used as "display-name".
+   *
+   * @return string
+   *   A RFC compliant version of the string, ready to be used as
+   *   "display-name" in mail originator header fields.
+   */
+  public static function formatDisplayName($string) {
+    // Make sure we don't process html-encoded characters. They may create
+    // unneeded trouble if left encoded, besides they will be correctly
+    // processed if decoded.
+    $string = Html::decodeEntities($string);
+
+    // If string contains non-ASCII characters it must be (short) encoded
+    // according to RFC-2047. The output of a "B" (Base64) encoded-word is
+    // always safe to be used as display-name.
+    $safe_display_name = Unicode::mimeHeaderEncode($string, TRUE);
+
+    // Encoded-words are always safe to be used as display-name because don't
+    // contain any RFC 2822 "specials" characters. However
+    // Unicode::mimeHeaderEncode() encodes a string only if it contains any
+    // non-ASCII characters, and leaves its value untouched (un-encoded) if
+    // ASCII only. For this reason in order to produce a valid display-name we
+    // still need to make sure there are no "specials" characters left.
+    if (preg_match('/[' . preg_quote(Mail::RFC_2822_SPECIALS) . ']/', $safe_display_name)) {
+
+      // If string is already quoted, it may or may not be escaped properly, so
+      // don't trust it and reset.
+      if (preg_match('/^"(.+)"$/', $safe_display_name, $matches)) {
+        $safe_display_name = str_replace(['\\\\', '\\"'], ['\\', '"'], $matches[1]);
+      }
+
+      // Transform the string in a RFC-2822 "quoted-string" by wrapping it in
+      // double-quotes. Also make sure '"' and '\' occurrences are escaped.
+      $safe_display_name = '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $safe_display_name) . '"';
+
+    }
+
+    return $safe_display_name;
+  }
+
+}
diff --git a/web/modules/webform/src/Utility/WebformAccessibilityHelper.php b/web/modules/webform/src/Utility/WebformAccessibilityHelper.php
new file mode 100644
index 0000000000000000000000000000000000000000..806c07a0442b5580f3a54d65063622a7c1443758
--- /dev/null
+++ b/web/modules/webform/src/Utility/WebformAccessibilityHelper.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Drupal\webform\Utility;
+
+/**
+ * Helper class webform accessibility methods.
+ */
+class WebformAccessibilityHelper {
+
+  /**
+   * Visually hide text using .visually-hidden class.
+   *
+   * The .visually-hidden class is used to render invisible content just for
+   * screen reader users.
+   *
+   * @param string|array $title
+   *   Text or #markup that should be visually hidden.
+   *
+   * @return array
+   *   A renderable array with the text wrapped in
+   *   <span class="visually-hidden">
+   *
+   * @see https://webaim.org/techniques/css/invisiblecontent/
+   */
+  public static function buildVisuallyHidden($title) {
+    if (is_array($title)) {
+      return $title + [
+        '#prefix' => '<span class="visually-hidden">',
+        '#suffix' => '</span>',
+      ];
+    }
+    else {
+      return [
+        '#markup' => $title,
+        '#prefix' => '<span class="visually-hidden">',
+        '#suffix' => '</span>',
+      ];
+    }
+  }
+
+  /**
+   * Aria hide text using aria-hidden attribute.
+   *
+   * The aria-hidden property tells screen-readers if they
+   * should ignore the element.
+   *
+   * @param string|array $title
+   *   Text or #markup that should be aria-hidden.
+   *
+   * @return array
+   *   A renderable array with the text wrapped in
+   *   <span aria-hidden="true">
+   *
+   * @see https://www.w3.org/TR/wai-aria-1.1/#aria-hidden
+   */
+  public static function buildAriaHidden($title) {
+    if (is_array($title)) {
+      return $title + [
+        '#prefix' => '<span aria-hidden="true">',
+        '#suffix' => '</span>',
+      ];
+    }
+    else {
+      return [
+        '#markup' => $title,
+        '#prefix' => '<span aria-hidden="true">',
+        '#suffix' => '</span>',
+      ];
+    }
+  }
+
+}
diff --git a/web/modules/webform/src/Utility/WebformArrayHelper.php b/web/modules/webform/src/Utility/WebformArrayHelper.php
index 096d241ef078d2db43f64ad539abcfea61288cb5..b20528b22d931837acd3d074eda4cb21c52c3ef7 100644
--- a/web/modules/webform/src/Utility/WebformArrayHelper.php
+++ b/web/modules/webform/src/Utility/WebformArrayHelper.php
@@ -378,4 +378,23 @@ public static function insertAfter(array &$array, $target_key, $new_key, $new_va
     $array = $new;
   }
 
+  /**
+   * Remove value from an array.
+   *
+   * @param array &$array
+   *   An array.
+   * @param mixed $value
+   *   A value.
+   *
+   * @see https://stackoverflow.com/questions/7225070/php-array-delete-by-value-not-key
+   */
+  public static function removeValue(array &$array, $value) {
+    if (($key = array_search($value, $array)) !== FALSE) {
+      unset($array[$key]);
+    }
+    if (static::isSequential($array)) {
+      array_values($array);
+    }
+  }
+
 }
diff --git a/web/modules/webform/src/Utility/WebformDateHelper.php b/web/modules/webform/src/Utility/WebformDateHelper.php
index c9468562642e02988be2d833f45b69169d4f7fee..9a6951e758f3f5c4c5ea529359900ad39c2be3b4 100644
--- a/web/modules/webform/src/Utility/WebformDateHelper.php
+++ b/web/modules/webform/src/Utility/WebformDateHelper.php
@@ -2,8 +2,10 @@
 
 namespace Drupal\webform\Utility;
 
+use Drupal\Core\Datetime\DateHelper;
 use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Form\OptGroup;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
 
 /**
  * Helper class webform date helper methods.
@@ -44,15 +46,15 @@ class WebformDateHelper {
    *   the user interface language for the page.
    *
    * @return string
-   *   A translated date string in the requested format.  An empty string
-   *   will be returned for empty timestamps.
+   *   A translated date string in the requested format. An empty string will be
+   *   returned for empty timestamps.
    *
    * @see \Drupal\Core\Datetime\DateFormatterInterface::format
    */
   public static function format($timestamp, $type = 'fallback', $format = '', $timezone = NULL, $langcode = NULL) {
     /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
     $date_formatter = \Drupal::service('date.formatter');
-    return $timestamp ? $date_formatter->format($timestamp, $type) : '';
+    return $timestamp ? $date_formatter->format($timestamp, $type, $format, $timezone, $langcode) : '';
   }
 
   /**
@@ -65,25 +67,34 @@ public static function format($timestamp, $type = 'fallback', $format = '', $tim
    *   The date/time object format as 'Y-m-d\TH:i:s'.
    */
   public static function formatStorage(DrupalDateTime $date) {
-    return $date->format(DATETIME_DATETIME_STORAGE_FORMAT);
+    return $date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
   }
 
   /**
-   * Check if date/time string is using a valid date/time format.
+   * Creates a date object from an input format with a translated date string.
    *
-   * @param string $time
-   *   A date/time string.
    * @param string $format
-   *   Format accepted by date().
+   *   PHP date() type format for parsing the input.
+   * @param mixed $time
+   *   A date string.
+   * @param mixed $timezone
+   *   PHP DateTimeZone object, string or NULL allowed.
+   * @param array $settings
+   *   An array of settings.
    *
-   * @return bool
-   *   TRUE is $time is in the accepted format.
+   * @return \Drupal\Core\Datetime\DrupalDateTime|bool
+   *   A new DateTimePlus object or FALSE if invalid date string.
    *
-   * @see http://stackoverflow.com/questions/19271381/correctly-determine-if-date-string-is-a-valid-date-in-that-format
+   * @see \Drupal\Core\Datetime\DrupalDateTime::__construct
    */
-  public static function isValidDateFormat($time, $format = 'Y-m-d') {
-    $datetime = \DateTime::createFromFormat($format, $time);
-    return ($datetime && $datetime->format($format) === $time);
+  public static function createFromFormat($format, $time, $timezone = NULL, array $settings = []) {
+    $english_time = WebformDateHelper::convertDateStringToEnglish($format, $time);
+    try {
+      return DrupalDateTime::createFromFormat($format, $english_time, $timezone, $settings);
+    }
+    catch (\Exception $exception) {
+      return FALSE;
+    }
   }
 
   /**
@@ -185,4 +196,65 @@ protected static function initIntervalOptions() {
     }
   }
 
+  /**
+   * Convert date string to English so that it can be parsed.
+   *
+   * @param string $format
+   *   PHP date() type format for parsing the input.
+   * @param string $value
+   *   A date string.
+   *
+   * @return string
+   *   A date string translated to English.
+   *
+   * @see https://stackoverflow.com/questions/36498186/php-datetimecreatefromformat-and-multi-languages
+   * @see core/modules/locale/locale.datepicker.js
+   */
+  protected static function convertDateStringToEnglish($format, $value) {
+    // Do not convert English date strings.
+    if (\Drupal::languageManager()->getCurrentLanguage()->getId() === 'en') {
+      return $value;
+    }
+
+    // F = A full textual representation of a month, such as January or March.
+    if (strpos($format, 'F') !== FALSE) {
+      $month_names_untranslated = DateHelper::monthNamesUntranslated();
+      $month_names_translated = DateHelper::monthNames();
+      foreach ($month_names_untranslated as $index => $month_name_untranslated) {
+        $value = str_ireplace((string) $month_names_translated[$index], $month_name_untranslated, $value);
+      }
+
+    }
+
+    // M =	A short textual representation of a month, three letters.
+    if (strpos($format, 'M') !== FALSE) {
+      $month_names_abbr_untranslated = DateHelper::monthNamesAbbrUntranslated();
+      $month_names_abbr_translated = DateHelper::monthNamesAbbr();
+      foreach ($month_names_abbr_untranslated as $index => $month_name_abbr_untranslated) {
+        $value = str_ireplace((string) $month_names_abbr_translated[$index], $month_name_abbr_untranslated, $value);
+      }
+    }
+
+    // l = A full textual representation of the day of the week.
+    if (strpos($format, 'l') !== FALSE) {
+      $week_days_untranslated = DateHelper::weekDaysUntranslated();
+      $week_days_translated = DateHelper::weekDays();
+      foreach ($week_days_untranslated as $index => $week_day_untranslated) {
+        $value = str_ireplace((string) $week_days_translated[$index], $week_day_untranslated, $value);
+      }
+    }
+
+    // D = A textual representation of a day, three letters.
+    if (strpos($format, 'D') !== FALSE) {
+      $week_days_abbr_untranslated = DateHelper::weekDaysUntranslated();
+      $week_days_abbr_translated = DateHelper::weekDaysAbbr();
+      foreach ($week_days_abbr_untranslated as $index => $week_day_abbr_untranslated) {
+        $week_days_abbr_untranslated[$index] = (string) substr($week_days_abbr_untranslated[$index], 0, 3);
+        $value = str_ireplace((string) $week_days_abbr_translated[$index], $week_days_abbr_untranslated[$index], $value);
+      }
+    }
+
+    return $value;
+  }
+
 }
diff --git a/web/modules/webform/src/Utility/WebformDialogHelper.php b/web/modules/webform/src/Utility/WebformDialogHelper.php
index 786a186aba711cfe983ef1cc87fa3181a5af1eef..0fc2da05cbd85aff7139644234fb9ac2a652069b 100644
--- a/web/modules/webform/src/Utility/WebformDialogHelper.php
+++ b/web/modules/webform/src/Utility/WebformDialogHelper.php
@@ -21,7 +21,7 @@ class WebformDialogHelper {
   /**
    * Width for normal dialog. (modal: 800px; off-canvas: 600px)
    *
-   * Used by: Add and edit element/handler, etc...
+   * Used by: Add and edit element/handler, etc…
    *
    * @var string
    */
@@ -30,7 +30,7 @@ class WebformDialogHelper {
   /**
    * Width for narrow dialog. (modal: 700px; off-canvas: 500px)
    *
-   * Used by: Duplicate and delete entity, notes, etc...
+   * Used by: Duplicate and delete entity, notes, etc…
    *
    * @var string
    */
@@ -43,7 +43,7 @@ class WebformDialogHelper {
    *   TRUE if outside_in.module is enabled and system trays are not disabled.
    */
   public static function useOffCanvas() {
-    return ((floatval(\Drupal::VERSION) >= 8.5) && !\Drupal::config('webform.settings')->get('ui.offcanvas_disabled')) ? TRUE : FALSE;
+    return (!\Drupal::config('webform.settings')->get('ui.offcanvas_disabled')) ? TRUE : FALSE;
   }
 
   /**
@@ -54,6 +54,9 @@ public static function useOffCanvas() {
    */
   public static function attachLibraries(array &$build) {
     $build['#attached']['library'][] = 'webform/webform.admin.dialog';
+    if (static::useOffCanvas()) {
+      $build['#attached']['library'][] = 'webform/webform.off_canvas';
+    }
     // @see \Drupal\webform\Element\WebformHtmlEditor::preRenderWebformHtmlEditor
     if (\Drupal::moduleHandler()->moduleExists('imce') && \Drupal\imce\Imce::access()) {
       $build['#attached']['library'][] = 'imce/drupal.imce.ckeditor';
@@ -90,9 +93,9 @@ public static function getModalDialogAttributes($width = self::DIALOG_NORMAL, ar
       'data-dialog-type' => 'modal',
       'data-dialog-options' => Json::encode([
         'width' => $width,
-        // .webform-modal is used to set the dialog's top position.
+        // .webform-ui-dialog is used to set the dialog's top position.
         // @see modules/sandbox/webform/css/webform.ajax.css
-        'dialogClass' => 'webform-modal',
+        'dialogClass' => 'webform-ui-dialog',
       ]),
     ];
   }
diff --git a/web/modules/webform/src/Utility/WebformElementHelper.php b/web/modules/webform/src/Utility/WebformElementHelper.php
index 4519448c0501fcee59af008fc4f05a612687e7eb..4f7d7ee3458010d859d9e8ff38c64c7bcaa98ce5 100644
--- a/web/modules/webform/src/Utility/WebformElementHelper.php
+++ b/web/modules/webform/src/Utility/WebformElementHelper.php
@@ -71,6 +71,86 @@ class WebformElementHelper {
    */
   protected static $ignoredSubPropertiesRegExp;
 
+  /**
+   * Determine if an element and its key is a renderable array.
+   *
+   * @param array|mixed $element
+   *   An element.
+   * @param string $key
+   *   The element key.
+   *
+   * @return bool
+   *   TRUE if an element and its key is a renderable array.
+   */
+  public static function isElement($element, $key) {
+    return (Element::child($key) && is_array($element));
+  }
+
+  /**
+   * Determine if an element has children.
+   *
+   * @param array|mixed $element
+   *   An element.
+   *
+   * @return bool
+   *   TRUE if an element has children.
+   *
+   * @see \Drupal\Core\Render\Element::children
+   */
+  public static function hasChildren($element) {
+    foreach ($element as $key => $value) {
+      if ($key === '' || $key[0] !== '#') {
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Determine if an element is a webform element and should be enhanced.
+   *
+   * @param array $element
+   *   An element.
+   *
+   * @return bool
+   *   TRUE if an element is a webform element.
+   */
+  public static function isWebformElement(array $element) {
+    if (isset($element['#webform_key']) || isset($element['#webform_element'])) {
+      return TRUE;
+    }
+    elseif (\Drupal::service('webform.request')->isWebformAdminRoute()) {
+      return TRUE;
+    }
+    else {
+      return FALSE;
+    }
+  }
+
+  /**
+   * Determine if a webform element is a specified #type.
+   *
+   * @param array $element
+   *   A webform element.
+   * @param string|array $type
+   *   An element type.
+   *
+   * @return bool
+   *   TRUE if a webform element is a specified #type.
+   */
+  public static function isType(array $element, $type) {
+    if (!isset($element['#type'])) {
+      return FALSE;
+    }
+
+    if (is_array($type)) {
+      return in_array($element['#type'], $type);
+    }
+    else {
+      return ($element['#type'] === $type);
+    }
+  }
+
   /**
    * Determine if a webform element's title is displayed.
    *
@@ -139,52 +219,22 @@ public static function setPropertyRecursive(array &$element, $property_key, $pro
   }
 
   /**
-   * Enhance select menu with the Select2 or the Chosen library.
+   * Process a form element and apply webform element specific enhancements.
    *
-   * Please Note: Select2 is preferred library for Webform administrative
-   * forms.
+   * This method allows any form API element to be enhanced using webform
+   * specific features include custom validation, external libraries,
+   * accessibility improvements, etc…
    *
    * @param array $element
-   *   A select element.
-   * @param bool $library
-   *   Flag to automatically detect and apply library.
+   *   An associative array containing an element with a #type property.
    *
    * @return array
-   *   The select element with Select2 or Chosen class and library attached.
+   *   The processed form element with webform element specific enhancements.
    */
-  public static function enhanceSelect(array &$element, $library = FALSE) {
-    // If automatic is FALSE, look at the element's #select2 and #chosen
-    // property.
-    if (!$library) {
-      if (isset($element['#select2'])) {
-        $library = 'select2';
-      }
-      elseif (isset($element['#chosen'])) {
-        $library = 'chosen';
-      }
-    }
-
-    if ($library === FALSE) {
-      return $element;
-    }
-
-    /** @var \Drupal\webform\WebformLibrariesManagerInterface $libaries_manager */
-    $libaries_manager = \Drupal::service('webform.libraries_manager');
-
-    // Add select2 library and classes.
-    if (($library === TRUE || $library === 'select2') && $libaries_manager->isIncluded('jquery.select2')) {
-      $element['#attached']['library'][] = 'webform/webform.element.select2';
-      $element['#attributes']['class'][] = 'js-webform-select2';
-      $element['#attributes']['class'][] = 'webform-select2';
-    }
-    // Add chosen library and classes.
-    elseif (($library === TRUE || $library === 'chosen') && $libaries_manager->isIncluded('jquery.chosen')) {
-      $element['#attached']['library'][] = 'webform/webform.element.chosen';
-      $element['#attributes']['class'][] = 'js-webform-chosen';
-      $element['#attributes']['class'][] = 'webform-chosen';
-    }
-
-    return $element;
+  public static function process(array &$element) {
+    /** @var \Drupal\webform\Plugin\WebformElementManagerInterface $element */
+    $element_manager = \Drupal::service('plugin.manager.webform.element');
+    return $element_manager->processElement($element);
   }
 
   /**
@@ -353,6 +403,11 @@ public static function merge(array &$elements, array $source_elements) {
    *   An associative array of translated element properties.
    */
   public static function applyTranslation(array &$element, array $translation) {
+    // Apply all translated properties to the element.
+    // This allows default properties to be translated, which includes
+    // composite element titles.
+    $element += $translation;
+
     foreach ($element as $key => &$value) {
       // Make sure to only merge properties.
       if (!Element::property($key) || empty($translation[$key])) {
@@ -385,7 +440,7 @@ public static function applyTranslation(array &$element, array $translation) {
   public static function getFlattened(array $elements) {
     $flattened_elements = [];
     foreach ($elements as $key => &$element) {
-      if (Element::property($key) || !is_array($element)) {
+      if (!WebformElementHelper::isElement($element, $key)) {
         continue;
       }
 
@@ -442,7 +497,7 @@ public static function convertToString($element) {
   /****************************************************************************/
   // ISSUE: Hidden elements still need to call #element_validate because
   // certain elements, including managed_file, checkboxes, password_confirm,
-  // etc..., will also massage the submitted values via #element_validate.
+  // etc…, will also massage the submitted values via #element_validate.
   //
   // SOLUTION: Call #element_validate for all hidden elements but suppresses
   // #element_validate errors.
@@ -492,7 +547,8 @@ public static function triggerElementValidate(array &$element, FormStateInterfac
     // @see \Drupal\Core\Form\FormValidator::doValidateForm
     foreach ($element['#_element_validate'] as $callback) {
       $complete_form = &$form_state->getCompleteForm();
-      call_user_func_array($form_state->prepareCallback($callback), [&$element, &$form_state, &$complete_form]);
+      $arguments = [&$element, &$form_state, &$complete_form];
+      call_user_func_array($form_state->prepareCallback($callback), $arguments);
     }
   }
 
@@ -513,7 +569,8 @@ public static function suppressElementValidate(array &$element, FormStateInterfa
     // @see \Drupal\Core\Form\FormValidator::doValidateForm
     foreach ($element['#_element_validate'] as $callback) {
       $complete_form = &$form_state->getCompleteForm();
-      call_user_func_array($form_state->prepareCallback($callback), [&$element, &$temp_form_state, &$complete_form]);
+      $arguments = [&$element, &$temp_form_state, &$complete_form];
+      call_user_func_array($form_state->prepareCallback($callback), $arguments);
     }
 
     // Get the temp webform state's values.
@@ -554,9 +611,12 @@ public static function setRequiredError(array $element, FormStateInterface $form
    * @return array
    *   An associative array containing an element's states.
    */
-  public static function getStates(array $element) {
-    // Composite and multiple elements use use a custom states wrapper
-    // which will changes '#states' to '#_webform_states'.
+  public static function &getStates(array &$element) {
+    // Processed elements store the original #states in '#_webform_states'.
+    // @see \Drupal\webform\WebformSubmissionConditionsValidator::buildForm
+    //
+    // Composite and multiple elements use a custom states wrapper
+    // which will change '#states' to '#_webform_states'.
     // @see \Drupal\webform\Utility\WebformElementHelper::fixStatesWrapper
     if (!empty($element['#_webform_states'])) {
       return $element['#_webform_states'];
@@ -565,7 +625,10 @@ public static function getStates(array $element) {
       return $element['#states'];
     }
     else {
-      return [];
+      // Return empty states variable to prevent the below notice.
+      // 'Only variable references should be returned by reference'.
+      $empty_states = [];
+      return $empty_states;
     }
   }
 
@@ -594,4 +657,33 @@ public static function getRequiredFromVisibleStates(array $element) {
     return $required_states;
   }
 
+  /**
+   * Randomoize an associative array of element values and disable page caching.
+   *
+   * @param array $values
+   *   An associative array of element values.
+   *
+   * @return array
+   *   Randomized associative array of element values.
+   */
+  public static function randomize(array $values) {
+    // Make sure randomized elements and options are never cached by the
+    // current page.
+    \Drupal::service('page_cache_kill_switch')->trigger();
+    return WebformArrayHelper::shuffle($values);
+  }
+
+  /**
+   * Form API callback. Remove unchecked options and returns an array of values.
+   */
+  public static function filterValues(array &$element, FormStateInterface $form_state, array &$completed_form) {
+    $values = $element['#value'];
+    $values = array_filter($values, function ($value) {
+      return $value !== 0;
+    });
+    $values = array_values($values);
+    $element['#value'] = $values;
+    $form_state->setValueForElement($element, $values);
+  }
+
 }
diff --git a/web/modules/webform/src/Utility/WebformFormHelper.php b/web/modules/webform/src/Utility/WebformFormHelper.php
index 08a5af2f149ebb42a44ccd36a2a923370326b33c..ea2bdefc57744fe99b95e192c70cd14ea47c8680 100644
--- a/web/modules/webform/src/Utility/WebformFormHelper.php
+++ b/web/modules/webform/src/Utility/WebformFormHelper.php
@@ -104,6 +104,7 @@ public static function buildTabs(array $form, array $tabs, $active_tab = '') {
         '#group' => 'tabs',
         '#attributes' => [
           'id' => 'webform-tab--' . $tab_name,
+          'class' => ['webform-tab'],
         ],
       ];
     }
@@ -137,7 +138,7 @@ public static function buildTabs(array $form, array $tabs, $active_tab = '') {
    *
    * @return array
    *   The values without default keys like
-   *   'form_build_id', 'form_token', 'form_id', 'op', 'actions', etc...
+   *   'form_build_id', 'form_token', 'form_id', 'op', 'actions', etc…
    */
   public static function cleanupFormStateValues(array $values, array $keys = []) {
     // Remove default FAPI values.
@@ -190,7 +191,7 @@ public static function &flattenElements(array &$build) {
    */
   protected static function flattenElementsRecursive(array &$build, array &$elements, array &$duplicate_element_keys) {
     foreach ($build as $key => &$element) {
-      if (Element::child($key) && is_array($element)) {
+      if (WebformElementHelper::isElement($element, $key)) {
         // If there are duplicate element keys create an array of referenced
         // elements.
         if (isset($elements[$key])) {
diff --git a/web/modules/webform/src/Utility/WebformObjectHelper.php b/web/modules/webform/src/Utility/WebformObjectHelper.php
new file mode 100644
index 0000000000000000000000000000000000000000..4aefe50c1f9ab3855426ceb6f7f2445871937f9a
--- /dev/null
+++ b/web/modules/webform/src/Utility/WebformObjectHelper.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\webform\Utility;
+
+/**
+ * Provides helper to operate on objects.
+ */
+class WebformObjectHelper {
+
+  /**
+   * Sort object by properties.
+   *
+   * @param object $object
+   *   An object.
+   *
+   * @return object
+   *   Object sorted by properties.
+   */
+  public static function sortByProperty($object) {
+    $array = (array) $object;
+    ksort($array);
+    return (object) $array;
+  }
+
+}
diff --git a/web/modules/webform/src/Utility/WebformOptionsHelper.php b/web/modules/webform/src/Utility/WebformOptionsHelper.php
index 3ed405df87f10c7595d5da770706693d8a8ed72a..06e16055c32f446c2897624845729402dc663b28 100644
--- a/web/modules/webform/src/Utility/WebformOptionsHelper.php
+++ b/web/modules/webform/src/Utility/WebformOptionsHelper.php
@@ -17,7 +17,7 @@ class WebformOptionsHelper {
   const DESCRIPTION_DELIMITER = ' -- ';
 
   /**
-   * Append option value to option text
+   * Append option value to option text.
    *
    * @param array $options
    *   An associative array of options.
diff --git a/web/modules/webform/src/Utility/WebformTextHelper.php b/web/modules/webform/src/Utility/WebformTextHelper.php
new file mode 100644
index 0000000000000000000000000000000000000000..dbe71ff358ad0feee7184ca45f083938d63dc4f1
--- /dev/null
+++ b/web/modules/webform/src/Utility/WebformTextHelper.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Drupal\webform\Utility;
+
+use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
+
+/**
+ * Provides helper to operate on strings.
+ */
+class WebformTextHelper {
+
+  /**
+   * CamelCase to Underscore name converter.
+   *
+   * @var \Symfony\Component\Serializer\NameConverter\NameConverterInterface
+   */
+  protected static $converter;
+
+  /**
+   * Get camel case to snake case converter.
+   *
+   * @return \Symfony\Component\Serializer\NameConverter\NameConverterInterface
+   *   Camel case to snake case converter.
+   */
+  protected static function getCamelCaseToSnakeCaseNameConverter() {
+    if (!isset(static::$converter)) {
+      static::$converter = new CamelCaseToSnakeCaseNameConverter();
+    }
+    return static::$converter;
+  }
+
+  /**
+   * Converts camel case to snake case (i.e. underscores).
+   *
+   * @param string $string
+   *   String to be converted.
+   *
+   * @return string
+   *   String with camel case converted to snake case.
+   */
+  public static function camelToSnake($string) {
+    return static::getCamelCaseToSnakeCaseNameConverter()->normalize($string);
+  }
+
+  /**
+   * Converts snake case to camel case.
+   *
+   * @param string $string
+   *   String to be converted.
+   *
+   * @return string
+   *   String with snake case converted to camel case.
+   */
+  public static function snakeToCamel($string) {
+    return static::getCamelCaseToSnakeCaseNameConverter()->denormalize($string);
+  }
+
+  /**
+   * Counts the number of words inside a string.
+   *
+   * This counts the number of words by counting the space between the words.
+   *
+   * str_word_count() is locale dependent and returns varying word counts
+   * based on the current language.
+   *
+   * This approach matches how the jQuery Text Counter Plugin counts words.
+   *
+   * @param string $string
+   *   The string.
+   *
+   * @return int
+   *   The number of words inside the string.
+   *
+   * @see str_word_count()
+   * @see https://github.com/ractoon/jQuery-Text-Counter
+   * @see $.textcounter.wordCount
+   */
+  public static function wordCount($string) {
+    return count(explode(' ', preg_replace('#\s+#', ' ', trim($string))));
+  }
+
+}
diff --git a/web/modules/webform/src/Utility/WebformXss.php b/web/modules/webform/src/Utility/WebformXss.php
new file mode 100644
index 0000000000000000000000000000000000000000..ae268e107239d1c523bff5463cefd318b160e084
--- /dev/null
+++ b/web/modules/webform/src/Utility/WebformXss.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\webform\Utility;
+
+use Drupal\Component\Utility\Xss;
+
+/**
+ * Provides webform helper to filter for cross-site scripting.
+ */
+class WebformXss {
+
+  /**
+   * Gets the list of HTML tags allowed by Xss::filterAdmin() with missing <label>, <fieldset>, <legend>, <font> tags.
+   *
+   * @return array
+   *   The list of HTML tags allowed by filterAdmin() with missing
+   *   <label>, <fieldset>, <legend>, <font> tags.
+   */
+  public static function getAdminTagList() {
+    $allowed_tags = Xss::getAdminTagList();
+    $allowed_tags[] = 'label';
+    $allowed_tags[] = 'fieldset';
+    $allowed_tags[] = 'legend';
+    $allowed_tags[] = 'font';
+    return $allowed_tags;
+  }
+
+  /**
+   * Gets the standard list of HTML tags allowed by Xss::filter() with missing <font> tag.
+   *
+   * @return array
+   *   The list of HTML tags allowed by Xss::filter() with missing <font> tag.
+   */
+  public static function getHtmlTagList() {
+    $allowed_tags = Xss::getHtmlTagList();
+    $allowed_tags[] = 'font';
+    return $allowed_tags;
+  }
+
+}
diff --git a/web/modules/webform/src/Utility/WebformYaml.php b/web/modules/webform/src/Utility/WebformYaml.php
index cc709ae5c4b8524c3c27e16714043601843d3df6..c3eaf86b41354dc8f926b423749cb00cabb3bc46 100644
--- a/web/modules/webform/src/Utility/WebformYaml.php
+++ b/web/modules/webform/src/Utility/WebformYaml.php
@@ -2,13 +2,42 @@
 
 namespace Drupal\webform\Utility;
 
+use Drupal\Component\Serialization\SerializationInterface;
 use Drupal\Core\Serialization\Yaml;
-use Symfony\Component\Yaml\Unescaper;
+use Symfony\Component\Yaml\Dumper;
+use Symfony\Component\Yaml\Yaml as SymfonyYaml;
 
 /**
  * Provides YAML tidy function.
  */
-class WebformYaml {
+class WebformYaml implements SerializationInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function encode($data) {
+    $dumper = new Dumper(2);
+    $yaml = $dumper->dump($data, PHP_INT_MAX, 0, SymfonyYaml::DUMP_EXCEPTION_ON_INVALID_TYPE | SymfonyYaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
+
+    // Remove return after array delimiter.
+    $yaml = preg_replace('#((?:\n|^)[ ]*-)\n[ ]+(\w|[\'"])#', '\1 \2', $yaml);
+
+    return trim($yaml);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function decode($raw) {
+    return Yaml::decode($raw);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getFileExtension() {
+    return 'yml';
+  }
 
   /**
    * Determine if string is valid YAML.
@@ -16,7 +45,7 @@ class WebformYaml {
    * @param string $yaml
    *   A YAML string.
    *
-   * @return boolean
+   * @return bool
    *   TRUE if string is valid YAML.
    */
   public static function isValid($yaml) {
@@ -51,50 +80,17 @@ public static function validate($yaml) {
    *
    * @return string
    *   The encoded data.
+   *
+   * @see https://www.drupal.org/project/drupal/issues/2844452
+   * @see \Drupal\Component\Serialization\YamlSymfony::encode
    */
   public static function tidy($yaml) {
-    static $unescaper;
-    if (!isset($unescaper)) {
-      $unescaper = new Unescaper();
-    }
-
-    // Remove return after array delimiter.
-    $yaml = preg_replace('#((?:\n|^)[ ]*-)\n[ ]+(\w|[\'"])#', '\1 \2', $yaml);
-
-    // Support YAML newlines preserved syntax via pipe (|).
-    $lines = explode(PHP_EOL, $yaml);
-    foreach ($lines as $index => $line) {
-      if (empty($line) || strpos($line, '\n') === FALSE) {
-        continue;
-      }
-
-      if (preg_match('/^([ ]*(?:- )?)([a-z_]+|\'[^\']+\'|"[^"]+"): (\'|")(.+)\3$/', $line, $match)) {
-        $prefix = $match[1];
-        $indent = str_repeat(' ', strlen($prefix));
-        $name = $match[2];
-        $quote = $match[3];
-        $value = $match[4];
-
-        if ($quote == "'") {
-          $value = rtrim($unescaper->unescapeSingleQuotedString($value));
-        }
-        else {
-          $value = rtrim($unescaper->unescapeDoubleQuotedString($value));
-        }
-
-        if (strpos($value, '<') === FALSE) {
-          $lines[$index] = $prefix . $name . ": |\n$indent  " . str_replace(PHP_EOL, "\n$indent  ", $value);
-        }
-        else {
-          $value = preg_replace('~\R~u', PHP_EOL, $value);
-          $value = preg_replace('#\s*</p>#', '</p>', $value);
-          $value = str_replace(PHP_EOL, "\n$indent  ", $value);
-          $lines[$index] = $prefix . $name . ": |\n$indent  " . $value;
-        }
-      }
-    }
-    $yaml = implode(PHP_EOL, $lines);
-    return trim($yaml);
+    // Converting carriage returns (\r\n) to basic returns (\n).
+    // [Yaml] don't split lines on carriage returns when dumping #25864.
+    // @see https://github.com/symfony/symfony/pull/25864
+    $yaml = str_replace('\r\n', '\n', $yaml);
+    $data = self::decode($yaml);
+    return self::encode($data);
   }
 
 }
diff --git a/web/modules/webform/src/WebformAccessRulesManager.php b/web/modules/webform/src/WebformAccessRulesManager.php
new file mode 100644
index 0000000000000000000000000000000000000000..feae2f058c14d9a4dfd7a8d88d916ed8358dfdf1
--- /dev/null
+++ b/web/modules/webform/src/WebformAccessRulesManager.php
@@ -0,0 +1,193 @@
+<?php
+
+namespace Drupal\webform;
+
+use Drupal\Component\Utility\SortArray;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\webform\Access\WebformAccessResult;
+
+/**
+ * The webform access rules manager service.
+ */
+class WebformAccessRulesManager implements WebformAccessRulesManagerInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * Module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * WebformAccessRulesManager constructor.
+   *
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   Module handler service.
+   */
+  public function __construct(ModuleHandlerInterface $module_handler) {
+    $this->moduleHandler = $module_handler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkWebformAccess($operation, AccountInterface $account, WebformInterface $webform) {
+    $access_rules = $this->getAccessRules($webform);
+    $cache_per_user = $this->cachePerUser($access_rules);
+
+    $condition = $this->checkAccessRules($operation, $account, $access_rules);
+    return WebformAccessResult::allowedIf($condition, $webform, $cache_per_user);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkWebformSubmissionAccess($operation, AccountInterface $account, WebformSubmissionInterface $webform_submission) {
+    $webform = $webform_submission->getWebform();
+
+    $access_rules = $this->getAccessRules($webform);
+    $cache_per_user = $this->cachePerUser($access_rules);
+
+    // Check operation.
+    if ($this->checkAccessRules($operation, $account, $access_rules)) {
+      return WebformAccessResult::allowed($webform_submission, $cache_per_user);
+    }
+
+    // Check *_own operation.
+    if ($webform_submission->isOwner($account)
+      && isset($access_rules[$operation . '_own'])
+      && $this->checkAccessRule($access_rules[$operation . '_own'], $account)) {
+      return WebformAccessResult::allowed($webform_submission, $cache_per_user);
+    }
+
+    // Check *_any operation.
+    if (isset($access_rules[$operation . '_any'])
+      && $this->checkAccessRule($access_rules[$operation . '_any'], $account)) {
+      return WebformAccessResult::allowed($webform_submission, $cache_per_user);
+    }
+
+    return WebformAccessResult::neutral($webform_submission, $cache_per_user);
+  }
+
+  /****************************************************************************/
+  // Get access rules methods.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultAccessRules() {
+    $access_rules = [];
+
+    foreach ($this->getAccessRulesInfo() as $access_rule => $info) {
+      $access_rules[$access_rule] = [
+        'roles' => $info['roles'],
+        'users' => $info['users'],
+        'permissions' => $info['permissions'],
+      ];
+    }
+
+    return $access_rules;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAccessRulesInfo() {
+    $access_rules = $this->moduleHandler->invokeAll('webform_access_rules');
+    $this->moduleHandler->alter('webform_access_rules', $access_rules);
+
+    // Set access rule default values.
+    foreach ($access_rules as $access_rule => $info) {
+      $access_rules[$access_rule] += [
+        'title' => NULL,
+        'description' => NULL,
+        'weight' => 0,
+        'roles' => [],
+        'users' => [],
+        'permissions' => [],
+      ];
+    }
+
+    uasort($access_rules, [SortArray::class, 'sortByWeightElement']);
+
+    return $access_rules;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAccessRules(WebformInterface $webform) {
+    return $webform->getAccessRules() + $this->getDefaultAccessRules();
+  }
+
+  /****************************************************************************/
+  // Check access rules methods.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkAccessRules($operation, AccountInterface $account, array $access_rules) {
+    // Check administer access rule and grant full access to user.
+    if ($this->checkAccessRule($access_rules['administer'], $account)) {
+      return TRUE;
+    }
+
+    // Check operation.
+    if (isset($access_rules[$operation])
+      && $this->checkAccessRule($access_rules[$operation], $account)) {
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Checks an access rule against a user account's roles and id.
+   *
+   * @param array $access_rule
+   *   An access rule.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user session for which to check access.
+   *
+   * @return bool
+   *   Returns a TRUE if access is allowed.
+   *
+   * @see \Drupal\webform\Plugin\WebformElementBase::checkAccessRule
+   */
+  protected function checkAccessRule(array $access_rule, AccountInterface $account) {
+    if (!empty($access_rule['roles']) && array_intersect($access_rule['roles'], $account->getRoles())) {
+      return TRUE;
+    }
+    elseif (!empty($access_rule['users']) && in_array($account->id(), $access_rule['users'])) {
+      return TRUE;
+    }
+    elseif (!empty($access_rule['permissions'])) {
+      foreach ($access_rule['permissions'] as $permission) {
+        if ($account->hasPermission($permission)) {
+          return TRUE;
+        }
+      }
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function cachePerUser(array $access_rules) {
+    foreach ($access_rules as $access_rule) {
+      if (!empty($access_rule['users'])) {
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+}
diff --git a/web/modules/webform/src/WebformAccessRulesManagerInterface.php b/web/modules/webform/src/WebformAccessRulesManagerInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..ce5bc6b1d051fd63b6800dc8bf48ce6080db53a4
--- /dev/null
+++ b/web/modules/webform/src/WebformAccessRulesManagerInterface.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Drupal\webform;
+
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Interface of webform access rules manager.
+ */
+interface WebformAccessRulesManagerInterface {
+
+  /**
+   * Check if operation is allowed through access rules for a given webform.
+   *
+   * @param string $operation
+   *   Operation to check.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   Account who is requesting the operation.
+   * @param \Drupal\webform\WebformInterface $webform
+   *   Webform on which the operation is requested.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   Access result.
+   */
+  public function checkWebformAccess($operation, AccountInterface $account, WebformInterface $webform);
+
+  /**
+   * Check if operation is allowed through access rules for a submission.
+   *
+   * @param string $operation
+   *   Operation to check.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   Account who is requesting the operation.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   Webform submission on which the operation is requested.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   Access result.
+   */
+  public function checkWebformSubmissionAccess($operation, AccountInterface $account, WebformSubmissionInterface $webform_submission);
+
+  /****************************************************************************/
+  // Get access rules methods.
+  /****************************************************************************/
+
+  /**
+   * Returns the webform default access rules.
+   *
+   * @return array
+   *   A structured array containing all the webform default access rules.
+   */
+  public function getDefaultAccessRules();
+
+  /**
+   * Retrieve a list of access rules from a webform.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   Webform whose access rules to retrieve.
+   *
+   * @return array
+   *   Associative array of access rules contained in the provided webform. Keys
+   *   are operation names whereas values are sub arrays with the following
+   *   structure:
+   *   - roles: (array) Array of roles that should have access to this operation
+   *   - users: (array) Array of UIDs that should have access to this operation
+   *   - permissions: (array) Array of permissions that should grant access to
+   *     this operation
+   */
+  public function getAccessRules(WebformInterface $webform);
+
+  /****************************************************************************/
+  // Check access rules methods.
+  /****************************************************************************/
+
+  /**
+   * Check access for a given operation and set of access rules.
+   *
+   * @param string $operation
+   *   Operation that is being requested.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   Account that is requesting access to the operation.
+   * @param array $access_rules
+   *   A set of access rules to check against.
+   *
+   * @return bool
+   *   TRUE if access is allowed and FALSE is access is denied.
+   */
+  public function checkAccessRules($operation, AccountInterface $account, array $access_rules);
+
+  /**
+   * Collect metadata on known access rules.
+   *
+   * @return array
+   *   Array that describes all known access rules. It will be keyed by access
+   *   rule machine-name and will contain sub arrays with the following
+   *   structure:
+   *   - title: (string) Human-friendly translated string that describes the
+   *     meaning of this access rule.
+   *   - description: (array) Renderable array that explains what this access rule
+   *     stands for. Defaults to an empty array.
+   *   - roles: (string[]) Array of role IDs that should be granted this access
+   *     rule by default. Defaults to an empty array.
+   *   - permissions: (string[]) Array of permissions that should be granted this
+   *     access rule by default. Defaults to an empty array.
+   */
+  public function getAccessRulesInfo();
+
+  /**
+   * Determine if access rules should be cached per user.
+   *
+   * @param array $access_rules
+   *   A set of access rules.
+   *
+   * @return bool
+   *   TRUE if access rules should be cached per user.
+   */
+  public function cachePerUser(array $access_rules);
+
+}
diff --git a/web/modules/webform/src/WebformAddonsManager.php b/web/modules/webform/src/WebformAddonsManager.php
index 2ba8cfd7c50f72a6bf340180e7587c4ff622dc96..3f5e3566d24a1e87a1eeb5f202cfe8e8f7dcd097 100644
--- a/web/modules/webform/src/WebformAddonsManager.php
+++ b/web/modules/webform/src/WebformAddonsManager.php
@@ -120,6 +120,10 @@ public function getCategories() {
   protected function initProjects() {
     $projects = [];
 
+    /**************************************************************************/
+    // Config.
+    /**************************************************************************/
+
     // Config: Drush CMI tools.
     $projects['drush_cmi_tools'] = [
       'title' => $this->t('Drush CMI tools'),
@@ -128,10 +132,18 @@ protected function initProjects() {
       'category' => 'config',
     ];
 
+    // Config: Config Entity Revisions.
+    $projects['config_entity_revisions'] = [
+      'title' => $this->t('Config Entity Revisions'),
+      'description' => $this->t('Provides an API for augmenting Configuration entities in Drupal 8.5 and later with revision and moderation support.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/config_entity_revisions'),
+      'category' => 'config',
+    ];
+
     // Config: Configuration Ignore.
     $projects['config_ignore'] = [
       'title' => $this->t('Config Ignore'),
-      'description' => $this->t('Ignore certain configuration during import'),
+      'description' => $this->t('Ignore certain configuration during import.'),
       'url' => Url::fromUri('https://www.drupal.org/project/config_ignore'),
       'category' => 'config',
     ];
@@ -142,17 +154,69 @@ protected function initProjects() {
       'description' => $this->t('Provides configuration filter for importing and exporting split config.'),
       'url' => Url::fromUri('https://www.drupal.org/project/config_split'),
       'category' => 'config',
+    ];
+
+    // Config: Multiline config.
+    $projects['multiline_config'] = [
+      'title' => $this->t('Multiline config'),
+      'description' => $this->t('Allows configuration strings to be exported as multiline instead of one long single line.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/multiline_config'),
+      'category' => 'config',
+    ];
+
+    // Config: Webform Config Ignore.
+    $projects['webform_config_ignore'] = [
+      'title' => $this->t('Webform Config Ignore'),
+      'description' => $this->t('Adds a filter to configuration import and export to skip webforms and webform options.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_config_ignore'),
+      'category' => 'config',
+    ];
+
+    // Config: Webform Config Key Value.
+    $projects['	webform_config_key_value'] = [
+      'title' => $this->t('Webform Config Key Value'),
+      'description' => $this->t('Use the KeyValueStorage to save webform config instead of yaml config storage, allowing webforms to be treated more like content than configuration and are excluded from the configuration imports/exports.'),
+      'url' => Url::fromUri('https://www.drupal.org/sandbox/thtas/2994250'),
+      'category' => 'config',
+    ];
+
+    /**************************************************************************/
+    // Element.
+    /**************************************************************************/
+
+    // Element: Address.
+    $projects['address'] = [
+      'title' => $this->t('Address'),
+      'description' => $this->t('Provides functionality for storing, validating and displaying international postal addresses.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/address'),
+      'category' => 'element',
       'recommended' => TRUE,
     ];
 
+    // Element: Loqate.
+    $projects['loqate'] = [
+      'title' => $this->t('Loqate'),
+      'description' => $this->t('Provides the webform element called Address Loqate which integration with Loqate (previously PCA/Addressy) address lookup.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/loqate'),
+      'category' => 'element',
+    ];
+
+    // Element: Webform Belgian National Insurance Number.
+    $projects['webform_rrn_nrn'] = [
+      'title' => $this->t('Webform Belgian National Insurance Number'),
+      'description' => $this->t('Provides webform fieldtype for the Belgian National Insurance Number.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_rrn_nrn'),
+      'category' => 'element',
+    ];
+
     // Element: Webform Composite Tools.
     $projects['webform_composite'] = [
       'title' => $this->t('Webform Composite Tools'),
-      'description' => $this->t("Provides a reusable composite element for use on webforms."),
+      'description' => $this->t('Provides a reusable composite element for use on webforms.'),
       'url' => Url::fromUri('https://www.drupal.org/project/webform_composite'),
       'category' => 'element',
     ];
-    
+
     // Element: Webform Checkboxes Table.
     $projects['webform_checkboxes_table'] = [
       'title' => $this->t('Webform Checkboxes Table'),
@@ -169,11 +233,27 @@ protected function initProjects() {
       'category' => 'element',
     ];
 
+    // Element: Webform DropzoneJS.
+    $projects['webform_dropzonejs'] = [
+      'title' => $this->t('Webform DropzoneJS'),
+      'description' => $this->t('Creates a new DropzoneJS element that you can add to webforms.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_dropzonejs'),
+      'category' => 'element',
+    ];
+
     // Element: Webform Handsontable.
     $projects['handsontable_yml_webform'] = [
       'title' => $this->t('Webform Handsontable'),
-      'description' => $this->t("Allows both the Drupal Form API and the Drupal 8 Webforms module to use the Excel-like Handsontable library."),
-      'url' => Url::fromUri('https://www.drupal.org/handsontable_yml_webform'),
+      'description' => $this->t('Allows both the Drupal Form API and the Drupal 8 Webforms module to use the Excel-like Handsontable library.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/handsontable_yml_webform'),
+      'category' => 'element',
+    ];
+
+    // Element: Webform IBAN field .
+    $projects['webform_iban_field'] = [
+      'title' => $this->t('Webform IBAN field '),
+      'description' => $this->t('Provides an IBAN Field to collect a valid IBAN number.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_iban_field'),
       'category' => 'element',
     ];
 
@@ -193,6 +273,22 @@ protected function initProjects() {
       'category' => 'element',
     ];
 
+    // Element: Range Slider.
+    $projects['range_slider'] = [
+      'title' => $this->t('Range Slider'),
+      'description' => $this->t('Integration with http://rangeslider.js.org.'),
+      'url' => Url::fromUri('https://github.com/baikho/RangeSlider'),
+      'category' => 'element',
+    ];
+
+    // Element: Webform RUT.
+    $projects['webform_rut'] = [
+      'title' => $this->t('Webform RUT'),
+      'description' => $this->t("Provides a RUT (A unique identification number assigned to natural or legal persons of Chile) element."),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_rut'),
+      'category' => 'element',
+    ];
+
     // Element: Webform Score.
     $projects['webform_score'] = [
       'title' => $this->t('Webform Score'),
@@ -201,22 +297,161 @@ protected function initProjects() {
       'category' => 'element',
     ];
 
+    // Element: Webform Select Collection.
+    $projects['webform_select_collection'] = [
+      'title' => $this->t('Webform Select Collection'),
+      'description' => $this->t('Provides a webform element that groups multiple select elements into single collection.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_select_collection'),
+      'category' => 'element',
+    ];
+
     // Element: Webform Simple Hierarchical Select.
     $projects['webform_shs'] = [
       'title' => $this->t('Webform Simple Hierarchical Select'),
-      'description' => $this->t("Integrates Simple Hierarchical Select module with Webform."),
+      'description' => $this->t('Integrates Simple Hierarchical Select module with Webform.'),
       'url' => Url::fromUri('https://www.drupal.org/project/webform_shs'),
       'category' => 'element',
     ];
 
+    /**************************************************************************/
+    // Enhancement.
+    /**************************************************************************/
+
+    // Enhancement: Formset.
+    $projects['formset'] = [
+      'title' => $this->t('Formset'),
+      'description' => $this->t('Enables the creation of webform sets.'),
+      'url' => Url::fromUri('https://github.com/simesy/formset'),
+      'category' => 'enhancement',
+    ];
+
+    // Enhancement: Webform Confirmation File.
+    $projects['webform_confirmation_file'] = [
+      'title' => $this->t('Webform Confirmation File'),
+      'description' => $this->t('Provides a webform handler that streams the contents of a file to a user after completing a webform.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_confirmation_file'),
+      'category' => 'enhancement',
+    ];
+
+    // Enhancement: Webform Counter.
+    $projects['webform_counter'] = [
+      'title' => $this->t('Webform Counter'),
+      'description' => $this->t('Provides Submissions Counter feature for webforms.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_counter'),
+      'category' => 'enhancement',
+    ];
+
+    // Enhancement: Webform Embed.
+    $projects['webform_embed'] = [
+      'title' => $this->t('Webform Embed'),
+      'description' => $this->t('Allows you to embed webforms within an iframe on another site.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_embed'),
+      'category' => 'enhancement',
+    ];
+
+    // Enhancement: Webform Extra Field.
+    $projects['webform_extra_field'] = [
+      'title' => $this->t('Webform Extra Field'),
+      'description' => $this->t('Provides an extra field for placing a webform in any entity display mode.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_extra_field'),
+      'category' => 'enhancement',
+    ];
+
+    // Enhancement: Webform Feedback.
+    $projects['webform_feedback'] = [
+      'title' => $this->t('Webform Feedback'),
+      'description' => $this->t('Provides a feedback button for your website which allows you to gather customer/client feedback.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_feedback'),
+      'category' => 'enhancement',
+    ];
+
+    // Enhancement: Webform Pre-populate.
+    $projects['webform_prepopulate'] = [
+      'title' => $this->t('Webform Pre-populate'),
+      'description' => $this->t('Pre-populate a Webform with an external data source without disclosing information via the URL.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_prepopulate'),
+      'category' => 'enhancement',
+    ];
+
+    // Enhancement: Webform Protected Downloads.
+    $projects['webform_protected_downloads'] = [
+      'title' => $this->t('Webform Protected Downloads'),
+      'description' => $this->t('Provides protected file downloads using webforms.'),
+      'url' => Url::fromUri('https://github.com/timlovrecic/Webform-Protected-Downloads'),
+      'category' => 'enhancement',
+    ];
+
     // Enhancement: Webform Wizard Full Title.
     $projects['webform_wizard_full_title'] = [
       'title' => $this->t('Webform Wizard Full Title'),
-      'description' => $this->t('Extends functionality of Webform so on wizard forms, the title of the wizard page can override the form title'),
+      'description' => $this->t('Extends functionality of Webform so on wizard forms, the title of the wizard page can override the form title.'),
       'url' => Url::fromUri('https://www.drupal.org/project/webform_wizard_full_title'),
       'category' => 'enhancement',
     ];
 
+    /**************************************************************************/
+    // Integrations.
+    /**************************************************************************/
+
+    // Integrations: Webform CiviCRM Integration.
+    $projects['webform_civicrm'] = [
+      'title' => $this->t('Webform CiviCRM Integration'),
+      'description' => $this->t('A powerful, flexible, user-friendly form builder for CiviCRM.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_civicrm'),
+      'category' => 'integration',
+      'recommended' => TRUE,
+    ];
+
+    /**************************************************************************/
+
+    // Integrations: Ansible.
+    $projects['ansible'] = [
+      'title' => $this->t('Ansible'),
+      'description' => $this->t('Run Ansible playbooks using a Webform handler.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/ansible'),
+      'category' => 'integration',
+    ];
+
+    // Integrations: Commerce Webform Order.
+    $projects['commerce_webform_order'] = [
+      'title' => $this->t('Commerce Webform Order'),
+      'description' => $this->t('Integrates Webform with Drupal Commerce and it allows creating orders with the submission data of a Webform via a Webform handler.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/commerce_webform_order'),
+      'category' => 'integration',
+    ];
+
+    // Integrations: Druminate Webforms.
+    $projects['druminate'] = [
+      'title' => $this->t('Druminate Webforms'),
+      'description' => $this->t('Allows editors to send webform submissions to Luminate Online Surveys.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/druminate'),
+      'category' => 'integration',
+    ];
+
+    // Integrations: Webform Eloqua.
+    $projects['webform_eloqua'] = [
+      'title' => $this->t('Webform Eloqua'),
+      'description' => $this->t('Integrates Drupal 8 Webforms with Oracle Eloqua.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_eloqua'),
+      'category' => 'integration',
+    ];
+
+    // Integrations: GraphQL Webform.
+    $projects['graphql_webform'] = [
+      'title' => $this->t('GraphQL Webform'),
+      'description' => $this->t('Provides GraphQL integration with the Webform module.'),
+      'url' => Url::fromUri('https://github.com/duartegarin/graphql_webform'),
+      'category' => 'integration',
+    ];
+
+    // Integrations: Headless Ninja React Webform.
+    $projects['hn-react-webform'] = [
+      'title' => $this->t('Headless Ninja React Webform'),
+      'description' => $this->t('With this awesome React component, you can render complete Drupal Webforms in React. With validation, easy custom styling and a modern, clean interface.'),
+      'url' => Url::fromUri('https://github.com/headless-ninja/hn-react-webform'),
+      'category' => 'integration',
+    ];
+
     // Integration: Webform HubSpot.
     $projects['hubspot'] = [
       'title' => $this->t('Webform HubSpot'),
@@ -233,6 +468,22 @@ protected function initProjects() {
       'category' => 'integration',
     ];
 
+    // Integrations: OpenInbound for Drupal.
+    $projects['openinbound'] = [
+      'title' => $this->t('OpenInbound for Drupal'),
+      'description' => $this->t('OpenInbound tracks contacts and their interactions on websites.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/openinbound'),
+      'category' => 'integration',
+    ];
+
+    // Integrations: Rules Webform.
+    $projects['rules_webform'] = [
+      'title' => $this->t('Rules Webform'),
+      'description' => $this->t("Provides integration of 'Rules' and 'Webform' modules. It enables to get access to webform submission data from rules. Also it provides possibility of altering and removing webform submission data from rules."),
+      'url' => Url::fromUri('https://www.drupal.org/project/rules_webform'),
+      'category' => 'integration',
+    ];
+
     // Integration: Webform iContact.
     $projects['webform_icontact'] = [
       'title' => $this->t('Webform iContact'),
@@ -241,6 +492,14 @@ protected function initProjects() {
       'category' => 'integration',
     ];
 
+    // Integration: Webform Cart.
+    $projects['webform_cart'] = [
+      'title' => $this->t('Webform Cart'),
+      'description' => $this->t('Allows you to add products to a webform submission.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_cart'),
+      'category' => 'integration',
+    ];
+
     // Integrations: Webform MailChimp.
     $projects['webform_mailchimp'] = [
       'title' => $this->t('Webform MailChimp'),
@@ -297,14 +556,16 @@ protected function initProjects() {
       'category' => 'integration',
     ];
 
-    // Integrations: OpenInbound for Drupal.
-    $projects['openinbound'] = [
-      'title' => $this->t('OpenInbound for Drupal'),
-      'description' => $this->t('OpenInbound tracks contacts and their interactions on websites.'),
-      'url' => Url::fromUri('https://www.drupal.org/project/openinbound'),
+    // Integrations: Webform User Registration.
+    $projects['webform_user_registration'] = [
+      'title' => $this->t('Webform User Registration'),
+      'description' => $this->t('Create a new user upon form submission.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_user_registration'),
       'category' => 'integration',
     ];
 
+    /**************************************************************************/
+
     // Integrations: Salesforce Web-to-Lead Webform Data Integration.
     $projects['sfweb2lead_webform'] = [
       'title' => $this->t('Salesforce Web-to-Lead Webform Data Integration'),
@@ -313,6 +574,26 @@ protected function initProjects() {
       'category' => 'integration',
     ];
 
+    // Integrations: Salesforce Marketing Cloud API Integration.
+    $projects['marketing_cloud'] = [
+      'title' => $this->t('Salesforce Marketing Cloud API Integration'),
+      'description' => $this->t('Gives Drupal the ability to communicate with Marketing Cloud.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/marketing_cloud'),
+      'category' => 'integration',
+    ];
+
+    // Integrations: Salesforce: Webform to Salesforce Leads.
+    $projects['webform_to_leads'] = [
+      'title' => $this->t('Salesforce: Webform to Salesforce Leads'),
+      'description' => $this->t('Extends the Webform module to allow the creation of a webform that feeds to your Salesforce.com Account'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_to_leads'),
+      'category' => 'integration',
+    ];
+
+    /**************************************************************************/
+    // Mail.
+    /**************************************************************************/
+
     // Mail: Mail System.
     $projects['mailsystem'] = [
       'title' => $this->t('Mail System'),
@@ -321,6 +602,22 @@ protected function initProjects() {
       'category' => 'mail',
     ];
 
+    // Mail: Mail System: SendGrid Integration.
+    $projects['sendgrid_integration'] = [
+      'title' => $this->t('SendGrid Integration <em>(requires Mail System)</em>'),
+      'description' => $this->t('Provides SendGrid Integration for the Drupal Mail System.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/sendgrid_integration'),
+      'category' => 'mail',
+    ];
+
+    // Mail: Mail System: Swift Mailer.
+    $projects['swiftmailer'] = [
+      'title' => $this->t('Swift Mailer <em>(requires Mail System)</em>'),
+      'description' => $this->t('Installs Swift Mailer as a mail system.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/swiftmailer'),
+      'category' => 'mail',
+    ];
+
     // Mail: Webform Mass Email.
     $projects['webform_mass_email'] = [
       'title' => $this->t('Webform Mass Email'),
@@ -329,6 +626,14 @@ protected function initProjects() {
       'category' => 'mail',
     ];
 
+    // Mail: Webform Send Multiple Emails.
+    $projects['webform_send_multiple_emails'] = [
+      'title' => $this->t('Webform Send Multiple Emails'),
+      'description' => $this->t('Extends the Webform module Email Handler to send individual emails when multiple recipients are added to the email "to" field.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_send_multiple_emails'),
+      'category' => 'mail',
+    ];
+
     // Mail: SMTP Authentication Support.
     $projects['smtp'] = [
       'title' => $this->t('SMTP Authentication Support'),
@@ -337,6 +642,10 @@ protected function initProjects() {
       'category' => 'mail',
     ];
 
+    /**************************************************************************/
+    // Multilingual.
+    /**************************************************************************/
+
     // Multilingual: Lingotek Translation.
     $projects['lingotek'] = [
       'title' => $this->t('Lingotek Translation.'),
@@ -345,6 +654,10 @@ protected function initProjects() {
       'category' => 'multilingual',
     ];
 
+    /**************************************************************************/
+    // Migrate.
+    /**************************************************************************/
+
     // Migrate: Webform Migrate.
     $projects['webform_migrate'] = [
       'title' => $this->t('Webform Migrate'),
@@ -354,6 +667,18 @@ protected function initProjects() {
       'recommended' => TRUE,
     ];
 
+    // Migrate: Webform Submission Import.
+    $projects['webform_submission_import'] = [
+      'title' => $this->t('Webform Submission Import'),
+      'description' => $this->t('Import CSV records into webform submissions data.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_submission_import'),
+      'category' => 'migrate',
+    ];
+
+    /**************************************************************************/
+    // Spam.
+    /**************************************************************************/
+
     // Spam: Antibot.
     $projects['antibot'] = [
       'title' => $this->t('Antibot'),
@@ -361,6 +686,7 @@ protected function initProjects() {
       'url' => Url::fromUri('https://www.drupal.org/project/antibot'),
       'category' => 'spam',
       'third_party_settings' => TRUE,
+      'recommended' => TRUE,
     ];
 
     // Spam: CAPTCHA.
@@ -372,6 +698,18 @@ protected function initProjects() {
       'recommended' => TRUE,
     ];
 
+    // Spam: Honeypot.
+    $projects['honeypot'] = [
+      'title' => $this->t('Honeypot'),
+      'description' => $this->t('Mitigates spam form submissions using the honeypot method.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/honeypot'),
+      'category' => 'spam',
+      'third_party_settings' => TRUE,
+      'recommended' => TRUE,
+    ];
+
+    /**************************************************************************/
+
     // Spam: CleanTalk.
     $projects['cleantalk'] = [
       'title' => $this->t('CleanTalk'),
@@ -380,13 +718,33 @@ protected function initProjects() {
       'category' => 'spam',
     ];
 
-    // Spam: Honeypot.
-    $projects['honeypot'] = [
-      'title' => $this->t('Honeypot'),
-      'description' => $this->t('Mitigates spam form submissions using the honeypot method.'),
-      'url' => Url::fromUri('https://www.drupal.org/project/honeypot'),
+    // Spam: Human Presence Form Protection.
+    $projects['hp'] = [
+      'title' => $this->t('Human Presence Form Protection'),
+      'description' => $this->t('Human Presence is a fraud prevention and form protection service that uses multiple overlapping strategies to fight form spam.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/hp'),
       'category' => 'spam',
-      'third_party_settings' => TRUE,
+    ];
+
+    /**************************************************************************/
+    // Submissions.
+    /**************************************************************************/
+
+    // Submissions: Webform Analysis.
+    $projects['webform_analysis'] = [
+      'title' => $this->t('Webform Analysis'),
+      'description' => $this->t('Used to obtain statistics on the results of form submissions.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_analysis'),
+      'category' => 'submission',
+      'recommended' => TRUE,
+    ];
+
+    // Submissions: Webform Query.
+    $projects['webform_query'] = [
+      'title' => $this->t('Webform Query'),
+      'description' => $this->t('Query webform submission data.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_query'),
+      'category' => 'submission',
       'recommended' => TRUE,
     ];
 
@@ -399,16 +757,9 @@ protected function initProjects() {
       'recommended' => TRUE,
     ];
 
-    // Submissions: Webform Analysis.
-    $projects['webform_analysis'] = [
-      'title' => $this->t('Webform Analysis'),
-      'description' => $this->t('Used to obtain statistics on the results of form submissions.'),
-      'url' => Url::fromUri('https://www.drupal.org/project/webform_analysis'),
-      'category' => 'submission',
-      'recommended' => TRUE,
-    ];
+    /**************************************************************************/
 
-    // Webform Invitation.
+    // Submissions: Webform Invitation.
     $projects['webform_invitation'] = [
       'title' => $this->t('Webform Invitation'),
       'description' => $this->t('Allows you to restrict submissions to a webform by generating codes (which may then be distributed e.g. by email to participants).'),
@@ -424,6 +775,14 @@ protected function initProjects() {
       'category' => 'submission',
     ];
 
+    // Submissions: Webform Queue.
+    $projects['webform_queue'] = [
+      'title' => $this->t('Webform Queue'),
+      'description' => $this->t('Posts form submissions into a Drupal queue.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_queue'),
+      'category' => 'submission',
+    ];
+
     // Submissions: Webform Sanitize.
     $projects['webform_sanitize'] = [
       'title' => $this->t('Webform Sanitize'),
@@ -440,14 +799,34 @@ protected function initProjects() {
       'category' => 'submission',
     ];
 
-    // Submissions: Webform Queue.
-    $projects['webform_queue'] = [
-      'title' => $this->t('Webform Queue'),
-      'description' => $this->t('Posts form submissions into a Drupal queue.'),
-      'url' => Url::fromUri('https://www.drupal.org/project/webform_queue'),
+    // Submissions: Webform Submission Change History.
+    $projects['webform_submission_change_history'] = [
+      'title' => $this->t('Webform Submission Change History'),
+      'description' => $this->t('Allows administrators to track notes on webform submissions.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_submission_change_history'),
       'category' => 'submission',
     ];
 
+    // Submissions: Webform Submissions Notification.
+    $projects['webform_digests'] = [
+      'title' => $this->t(' Webform Submissions Notification'),
+      'description' => $this->t('Adds a daily digest email for webform submissions.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_digests'),
+      'category' => 'submission',
+    ];
+
+    // Submissions: Webform Submission Files Download.
+    $projects['webform_submission_files_download'] = [
+      'title' => $this->t(' Webform Submission Files Download'),
+      'description' => $this->t('Allows you to download files attached to a single submission'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_submission_files_download'),
+      'category' => 'submission',
+    ];
+
+    /**************************************************************************/
+    // REST.
+    /**************************************************************************/
+
     // REST: Webform REST.
     $projects['webform_rest'] = [
       'title' => $this->t('Webform REST'),
@@ -456,21 +835,17 @@ protected function initProjects() {
       'category' => 'rest',
     ];
 
-    // Utility: Webform Encrypt.
-    $projects['wf_encrypt'] = [
-      'title' => $this->t('Webform Encrypt'),
-      'description' => $this->t('Provides encryption for webform elements.'),
-      'url' => Url::fromUri('https://www.drupal.org/project/webform_encrypt'),
-      'category' => 'utility',
+    // REST: Webform JSON Schema.
+    $projects['webform_jsonschema'] = [
+      'title' => $this->t('Webform JSON Schema'),
+      'description' => $this->t('Expose webforms as JSON Schema, UI Schema, and Form Data. Make webforms work with react-jsonschema-form.'),
+      'url' => Url::fromUri('https://github.com/AmazeeLabs/webform_jsonschema'),
+      'category' => 'rest',
     ];
 
-    // Utility: Webform Ip Track.
-    $projects['webform_ip_track'] = [
-      'title' => $this->t('Webform Ip Track'),
-      'description' => $this->t('Ip Location details as custom tokens to use in webform submission values.'),
-      'url' => Url::fromUri('https://www.drupal.org/project/webform_ip_track'),
-      'category' => 'utility',
-    ];
+    /**************************************************************************/
+    // Utility.
+    /**************************************************************************/
 
     // Utility: IMCE.
     $projects['imce'] = [
@@ -478,7 +853,7 @@ protected function initProjects() {
       'description' => $this->t('IMCE is an image/file uploader and browser that supports personal directories and quota.'),
       'url' => Url::fromUri('https://www.drupal.org/project/imce'),
       'category' => 'utility',
-      'install' => TRUE,
+      'install' => $this->t('The IMCE module makes it easier to update images to webforms and elements.'),
       'recommended' => TRUE,
     ];
 
@@ -488,10 +863,56 @@ protected function initProjects() {
       'description' => $this->t('Provides a user interface for the Token API and some missing core tokens.'),
       'url' => Url::fromUri('https://www.drupal.org/project/token'),
       'category' => 'utility',
-      'install' => TRUE,
+      'install' => $this->t('The Token module allows site builders to browser available webform-related tokens.'),
       'recommended' => TRUE,
     ];
 
+    /**************************************************************************/
+
+    // Utility: Googalytics Webform.
+    $projects['ga_webform'] = [
+      'title' => $this->t('Googalytics Webform'),
+      'description' => $this->t('Provides integration for Webform into Googalytics module.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/ga_webform'),
+      'category' => 'utility',
+    ];
+
+    // Utility: EU Cookie Compliance.
+    $projects['eu_cookie_compliance'] = [
+      'title' => $this->t('EU Cookie Compliance'),
+      'description' => $this->t('This module aims at making the website compliant with the new EU cookie regulation.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/eu_cookie_compliance'),
+      'category' => 'utility',
+    ];
+
+    // Utility: General Data Protection Regulation Compliance.
+    $projects['gdpr_compliance'] = [
+      'title' => $this->t('General Data Protection Regulation Compliance'),
+      'description' => $this->t('Provides Basic GDPR Compliance use cases via form checkboxes, pop-up alert, and a policy page.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/gdpr_compliance'),
+      'category' => 'utility',
+    ];
+
+    // Utility: Webform Encrypt.
+    $projects['wf_encrypt'] = [
+      'title' => $this->t('Webform Encrypt'),
+      'description' => $this->t('Provides encryption for webform elements.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_encrypt'),
+      'category' => 'utility',
+    ];
+
+    // Utility: Webform Ip Track.
+    $projects['webform_ip_track'] = [
+      'title' => $this->t('Webform Ip Track'),
+      'description' => $this->t('Ip Location details as custom tokens to use in webform submission values.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/webform_ip_track'),
+      'category' => 'utility',
+    ];
+
+    /**************************************************************************/
+    // Validation.
+    /**************************************************************************/
+
     // Validation: Clientside Validation.
     $projects['clientside_validation'] = [
       'title' => $this->t('Clientside Validation'),
@@ -501,6 +922,14 @@ protected function initProjects() {
       'recommended' => TRUE,
     ];
 
+    // Validation: Telephone Validation.
+    $projects['telephone_validation'] = [
+      'title' => $this->t('Telephone Validation'),
+      'description' => $this->t('Provides validation for tel form element.'),
+      'url' => Url::fromUri('https://www.drupal.org/project/telephone_validation'),
+      'category' => 'validation',
+    ];
+
     // Validation: Validators.
     $projects['validators'] = [
       'title' => $this->t('Validators'),
@@ -509,6 +938,10 @@ protected function initProjects() {
       'category' => 'validation',
     ];
 
+    /**************************************************************************/
+    // Workflow.
+    /**************************************************************************/
+
     // Workflow: Maestro.
     $projects['maestro'] = [
       'title' => $this->t('Maestro Workflow Engine'),
diff --git a/web/modules/webform/src/WebformContributeManager.php b/web/modules/webform/src/WebformContributeManager.php
index 6375d5995365ca9899a6fa0b58e8881bf995141f..aa6051040c1b2dc821a2a13023cd08c07c49c579 100644
--- a/web/modules/webform/src/WebformContributeManager.php
+++ b/web/modules/webform/src/WebformContributeManager.php
@@ -333,7 +333,6 @@ public function getMembership() {
     return $membership;
   }
 
-
   /**
    * {@inheritdoc}
    */
@@ -472,7 +471,7 @@ public function getStyle() {
    *   The remote URI.
    *
    * @return mixed|null
-   *   The returned data. Tequests to *.json files will be decoded.
+   *   The returned data. Requests to *.json files will be decoded.
    */
   protected function get($uri) {
     if (isset($this->cachedData[$uri])) {
diff --git a/web/modules/webform/src/WebformEmailProvider.php b/web/modules/webform/src/WebformEmailProvider.php
index 02a62dab10228195ad347f3fb7e992c026e69f7d..097e8ef054985b2feae8480b62e33e76ece833e8 100644
--- a/web/modules/webform/src/WebformEmailProvider.php
+++ b/web/modules/webform/src/WebformEmailProvider.php
@@ -66,7 +66,7 @@ public function getModules() {
   public function check() {
     // Don't override the system.mail.interface.webform if the default interface
     // is the 'test_mail_collector'.
-    if ($this->configFactory->get('system.mail')->get('interface.default') == 'test_mail_collector') {
+    if ($this->configFactory->get('system.mail')->get('interface.default') === 'test_mail_collector') {
       return $this->uninstall();
     }
 
@@ -81,7 +81,7 @@ public function check() {
     // Finally, check if the default mail interface and see if it still uses the
     // php_mail. This check allow unknown contrib modules to handle sending
     // HTML emails.
-    if ($this->configFactory->get('system.mail')->get('interface.default') == 'php_mail') {
+    if ($this->configFactory->get('system.mail')->get('interface.default') === 'php_mail') {
       return $this->install();
     }
     else {
@@ -93,7 +93,7 @@ public function check() {
    * {@inheritdoc}
    */
   public function installed() {
-    return ($this->configFactory->get('system.mail')->get('interface.webform') == 'webform_php_mail');
+    return ($this->configFactory->get('system.mail')->get('interface.webform') === 'webform_php_mail');
   }
 
   /**
@@ -102,8 +102,10 @@ public function installed() {
   public function install() {
     $config = $this->configFactory->getEditable('system.mail');
     $mail_plugins = $config->get('interface');
-    $mail_plugins['webform'] = 'webform_php_mail';
-    $config->set('interface', $mail_plugins)->save();
+    if (!isset($mail_plugins['webform']) || $mail_plugins['webform'] !== 'webform_php_mail') {
+      $mail_plugins['webform'] = 'webform_php_mail';
+      $config->set('interface', $mail_plugins)->save();
+    }
   }
 
   /**
@@ -112,8 +114,10 @@ public function install() {
   public function uninstall() {
     $config = $this->configFactory->getEditable('system.mail');
     $mail_plugins = $config->get('interface');
-    unset($mail_plugins['webform']);
-    $config->set('interface', $mail_plugins)->save();
+    if (isset($mail_plugins['webform'])) {
+      unset($mail_plugins['webform']);
+      $config->set('interface', $mail_plugins)->save();
+    }
   }
 
   /**
diff --git a/web/modules/webform/src/WebformEntityAccessControlHandler.php b/web/modules/webform/src/WebformEntityAccessControlHandler.php
index 897bd689f6f9b15a345fabb3725d5960aa47c420..3fd0b54f1ad78379b3b91d0e371207f6f925d1a9 100644
--- a/web/modules/webform/src/WebformEntityAccessControlHandler.php
+++ b/web/modules/webform/src/WebformEntityAccessControlHandler.php
@@ -2,13 +2,14 @@
 
 namespace Drupal\webform;
 
-use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\AccessResultReasonInterface;
 use Drupal\Core\Entity\EntityAccessControlHandler;
 use Drupal\Core\Entity\EntityHandlerInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\webform\Access\WebformAccessResult;
 use Drupal\webform\Plugin\WebformSourceEntityManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\RequestStack;
@@ -41,6 +42,13 @@ class WebformEntityAccessControlHandler extends EntityAccessControlHandler imple
    */
   protected $webformSourceEntityManager;
 
+  /**
+   * Webform access rules manager service.
+   *
+   * @var \Drupal\webform\WebformAccessRulesManagerInterface
+   */
+  protected $accessRulesManager;
+
   /**
    * WebformEntityAccessControlHandler constructor.
    *
@@ -52,13 +60,16 @@ class WebformEntityAccessControlHandler extends EntityAccessControlHandler imple
    *   The entity type manager.
    * @param \Drupal\webform\Plugin\WebformSourceEntityManagerInterface $webform_source_entity_manager
    *   Webform source entity plugin manager.
+   * @param \Drupal\webform\WebformAccessRulesManagerInterface $access_rules_manager
+   *   Webform access rules manager service.
    */
-  public function __construct(EntityTypeInterface $entity_type, RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, WebformSourceEntityManagerInterface $webform_source_entity_manager) {
+  public function __construct(EntityTypeInterface $entity_type, RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, WebformSourceEntityManagerInterface $webform_source_entity_manager, WebformAccessRulesManagerInterface $access_rules_manager) {
     parent::__construct($entity_type);
 
     $this->requestStack = $request_stack;
     $this->entityTypeManager = $entity_type_manager;
     $this->webformSourceEntityManager = $webform_source_entity_manager;
+    $this->accessRulesManager = $access_rules_manager;
   }
 
   /**
@@ -69,7 +80,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
       $entity_type,
       $container->get('request_stack'),
       $container->get('entity_type.manager'),
-      $container->get('plugin.manager.webform.source_entity')
+      $container->get('plugin.manager.webform.source_entity'),
+      $container->get('webform.access_rules_manager')
     );
   }
 
@@ -77,11 +89,15 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
    * {@inheritdoc}
    */
   protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
-    if ($account->hasPermission('create webform')) {
-      return AccessResult::allowed()->cachePerPermissions();
+    // Check 'administer webform' and 'create webform' permissions.
+    if ($account->hasPermission('administer webform')) {
+      return WebformAccessResult::allowed();
+    }
+    elseif ($account->hasPermission('create webform')) {
+      return WebformAccessResult::allowed();
     }
     else {
-      return parent::checkCreateAccess($account, $context, $entity_bundle);
+      return WebformAccessResult::neutral();
     }
   }
 
@@ -90,100 +106,185 @@ protected function checkCreateAccess(AccountInterface $account, array $context,
    */
   public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
     /** @var \Drupal\webform\WebformInterface $entity */
-    // Check 'view' using 'create' custom webform submission access rules.
-    // Viewing a webform is the same as creating a webform submission.
-    if ($operation == 'view') {
-      return AccessResult::allowed();
+
+    // Check 'administer webform' permission.
+    if ($account->hasPermission('administer webform')) {
+      return WebformAccessResult::allowed();
     }
 
-    $uid = $entity->getOwnerId();
-    $is_owner = ($account->isAuthenticated() && $account->id() == $uid);
-    // Check if 'update' or 'delete' of 'own' or 'any' webform is allowed.
+    // Check 'administer' access rule.
+    if ($account->isAuthenticated()) {
+      $administer_access_result = $this->accessRulesManager->checkWebformAccess('administer', $account, $entity);
+      if ($administer_access_result->isAllowed()) {
+        return $administer_access_result;
+      }
+    }
+
+    $is_owner = ($account->id() == $entity->getOwnerId());
+
+    // Check 'view' operation use 'submission_create' when viewing rendered
+    // HTML webform or use access 'configuration' when requesting a
+    // webform's configuration via REST or JSON API.
+    // @see https://www.drupal.org/project/webform/issues/2956771
+    if ($operation === 'view') {
+      // Check is current request if for HTML.
+      $is_html = ($this->requestStack->getCurrentRequest()->getRequestFormat() === 'html');
+      // Make sure JSON API 1.x requests format which is 'html' is
+      // detected properly.
+      // @see https://www.drupal.org/project/jsonapi/issues/2877584
+      $is_jsonapi = (strpos($this->requestStack->getCurrentRequest()->getPathInfo(), '/jsonapi/') === 0) ? TRUE : FALSE;
+      if ($is_html && !$is_jsonapi) {
+        $access_result = $this->accessRulesManager->checkWebformAccess('create', $account, $entity);
+      }
+      else {
+        if ($account->hasPermission('access any webform configuration') || ($account->hasPermission('access own webform configuration') && $is_owner)) {
+          $access_result = WebformAccessResult::allowed($entity, TRUE);
+        }
+        else {
+          $access_result = $this->accessRulesManager->checkWebformAccess('configuration', $account, $entity);
+        }
+      }
+      if ($access_result instanceof AccessResultReasonInterface) {
+        $access_result->setReason('Access to webform configuration is required.');
+      }
+      return $access_result->addCacheContexts(['url.path', 'request_format']);
+    }
+
+    // Check if 'update', or 'delete' of 'own' or 'any' webform is allowed.
     if ($account->isAuthenticated()) {
-      $has_administer = $entity->checkAccessRules('administer', $account);
       switch ($operation) {
         case 'test':
         case 'update':
-          if ($has_administer->isAllowed() || $account->hasPermission('edit any webform') || ($account->hasPermission('edit own webform') && $is_owner)) {
-            return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity)->addCacheableDependency($has_administer);
+          if ($account->hasPermission('edit any webform') || ($account->hasPermission('edit own webform') && $is_owner)) {
+            return WebformAccessResult::allowed($entity, TRUE);
           }
           break;
 
         case 'duplicate':
-          if ($has_administer->isAllowed() || $account->hasPermission('create webform') && ($entity->isTemplate() || ($account->hasPermission('edit any webform') || ($account->hasPermission('edit own webform') && $is_owner)))) {
-            return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity)->addCacheableDependency($has_administer);
+          if ($account->hasPermission('create webform') && ($entity->isTemplate() || ($account->hasPermission('edit any webform') || ($account->hasPermission('edit own webform') && $is_owner)))) {
+            return WebformAccessResult::allowed($entity, TRUE);
           }
           break;
 
         case 'delete':
-          if ($has_administer->isAllowed() || $account->hasPermission('delete any webform') || ($account->hasPermission('delete own webform') && $is_owner)) {
-            return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity)->addCacheableDependency($has_administer);
+          if ($account->hasPermission('delete any webform') || ($account->hasPermission('delete own webform') && $is_owner)) {
+            return WebformAccessResult::allowed($entity, TRUE);
           }
           break;
       }
     }
 
-    // Check test operation.
-    if ($operation == 'test') {
-      $access_rules = $entity->checkAccessRules($operation, $account);
-      if ($access_rules->isAllowed()) {
-        return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($access_rules);
-      }
+    // Check webform access rules.
+    $rules_access_result = $this->accessRulesManager->checkWebformAccess($operation, $account, $entity);
+    if ($rules_access_result->isAllowed()) {
+      return $rules_access_result;
     }
 
     // Check submission_* operation.
     if (strpos($operation, 'submission_') === 0) {
-      // Allow users with 'view any webform submission' or
-      // 'administer webform submission' to view all submissions.
-      if ($operation == 'submission_view_any' && ($account->hasPermission('view any webform submission') || $account->hasPermission('administer webform submission'))) {
-        return AccessResult::allowed()->cachePerPermissions();
+      // Grant user with administer webform submission access to do whatever he
+      // likes on the submission operations.
+      if ($account->hasPermission('administer webform submission')) {
+        return WebformAccessResult::allowed();
       }
 
-      // Allow users with 'view own webform submission' to view own submissions.
-      if ($account->hasPermission('view own webform submission') && $is_owner) {
-        return AccessResult::allowed()->cachePerUser()->addCacheableDependency($entity);
+      // Allow users with 'view any webform submission' or
+      // 'administer webform submission' to view all submissions.
+      if ($operation === 'submission_view_any' && ($account->hasPermission('view any webform submission') || $account->hasPermission('administer webform submission'))) {
+        return WebformAccessResult::allowed();
       }
 
       // Allow users with 'view own webform submission' to view own submissions.
-      if ($operation == 'submission_view_own' && $account->hasPermission('view own webform submission')) {
-        return AccessResult::allowed()->cachePerPermissions();
+      if ($operation === 'submission_view_own' && $account->hasPermission('view own webform submission')) {
+        return WebformAccessResult::allowed();
       }
 
-      // Allow (secure) token to bypass submission page and create access controls.
       if (in_array($operation, ['submission_page', 'submission_create'])) {
+        /** @var \Drupal\webform\WebformSubmissionStorageInterface $submission_storage */
+        $submission_storage = $this->entityTypeManager->getStorage('webform_submission');
+
+        // Check limit total unique access.
+        // @see \Drupal\webform\WebformSubmissionForm::setEntity
+        if ($entity->getSetting('limit_total_unique')) {
+          $source_entity = $this->webformSourceEntityManager->getSourceEntity('webform');
+          $last_submission = $submission_storage->getLastSubmission($entity, $source_entity, NULL, ['in_draft' => FALSE]);
+          if ($last_submission && $last_submission->access('update')) {
+            return WebformAccessResult::allowed($last_submission);
+          }
+        }
+
+        // Check limit user unique access.
+        // @see \Drupal\webform\WebformSubmissionForm::setEntity
+        if ($entity->getSetting('limit_user_unique')) {
+          // Require user to be authenticated to access a unique submission.
+          if (!$account->isAuthenticated()) {
+            return WebformAccessResult::forbidden($entity);
+          }
+          $source_entity = $this->webformSourceEntityManager->getSourceEntity('webform');
+          $last_submission = $submission_storage->getLastSubmission($entity, $source_entity, $account, ['in_draft' => FALSE]);
+          if ($last_submission && $last_submission->access('update')) {
+            return WebformAccessResult::allowed($last_submission);
+          }
+        }
+
+        // Allow (secure) token to bypass submission page and create access controls.
         $token = $this->requestStack->getCurrentRequest()->query->get('token');
         if ($token && $entity->isOpen()) {
-          /** @var \Drupal\webform\WebformSubmissionStorageInterface $submission_storage */
-          $submission_storage = $this->entityTypeManager->getStorage('webform_submission');
-
-          $source_entity = $this->webformSourceEntityManager->getSourceEntity(['webform']);
+          $source_entity = $this->webformSourceEntityManager->getSourceEntity('webform');
           if ($submission = $submission_storage->loadFromToken($token, $entity, $source_entity)) {
-            return AccessResult::allowed()->addCacheableDependency($submission)->addCacheableDependency($entity)->addCacheContexts(['url']);
+            return WebformAccessResult::allowed($submission)
+              ->addCacheContexts(['url']);
           }
         }
       }
 
-      // Completely block access to a template if the user can't create new
-      // Webforms.
-      if ($operation == 'submission_page' && $entity->isTemplate()) {
+      // The "page" operation is the same as "create" but requires that the
+      // Webform is allowed to be displayed as dedicated page.
+      // Used by the 'entity.webform.canonical' route.
+      if ($operation === 'submission_page') {
+        // Completely block access to a template if the user can't create new
+        // Webforms.
         $create_access = $entity->access('create', $account, TRUE);
-        if (!$create_access->isAllowed()) {
-          return AccessResult::forbidden()->addCacheableDependency($entity)->addCacheableDependency($create_access);
+        if ($entity->isTemplate() && !$create_access->isAllowed()) {
+          return WebformAccessResult::forbidden($entity)
+            ->addCacheableDependency($create_access);
         }
+
+        // Block access if the webform does not have a page URL.
+        if (!$entity->getSetting('page')) {
+          $source_entity = $this->webformSourceEntityManager->getSourceEntity('webform');
+          if (!$source_entity) {
+            return WebformAccessResult::forbidden($entity);
+          }
+        }
+      }
+
+      // Convert submission 'page' to corresponding 'create' access rule.
+      $submission_operation = str_replace('submission_page', 'submission_create', $operation);
+      // Remove 'submission_*' prefix.
+      $submission_operation = str_replace('submission_', '', $submission_operation);
+
+      // Check webform submission access rules.
+      $submission_access_result = $this->accessRulesManager->checkWebformAccess($submission_operation, $account, $entity);
+      if ($submission_access_result->isAllowed()) {
+        return $submission_access_result;
       }
 
-      // Check custom webform submission access rules.
-      $update_access = $this->checkAccess($entity, 'update', $account);
-      $access_rules = $entity->checkAccessRules(str_replace('submission_', '', $operation), $account);
-      if ($update_access->isAllowed() || $access_rules->isAllowed()) {
-        return AccessResult::allowed()->addCacheableDependency($update_access)->addCacheableDependency($access_rules);
+      // Check webform 'update' access.
+      $update_access_result = $this->checkAccess($entity, 'update', $account);
+      if ($update_access_result->isAllowed()) {
+        return $update_access_result;
       }
     }
 
-    $access_result = parent::checkAccess($entity, $operation, $account);
-    // Make sure the webform is added as a cache dependency.
-    $access_result->addCacheableDependency($entity);
-    return $access_result;
+    // NOTE: Not calling parent::checkAccess().
+    // @see \Drupal\Core\Entity\EntityAccessControlHandler::checkAccess
+    if ($operation === 'delete' && $entity->isNew()) {
+      return WebformAccessResult::forbidden($entity);
+    }
+    else {
+      return WebformAccessResult::neutral($entity);
+    }
   }
 
 }
diff --git a/web/modules/webform/src/WebformEntityAddForm.php b/web/modules/webform/src/WebformEntityAddForm.php
index 6e5cedeba417e68b9a2795bc5d51b785b372e3ad..1de04f9eb7f011467fc03e5351d50aa9e7f0b07a 100644
--- a/web/modules/webform/src/WebformEntityAddForm.php
+++ b/web/modules/webform/src/WebformEntityAddForm.php
@@ -87,6 +87,17 @@ public function form(array $form, FormStateInterface $form_state) {
       '#empty_option' => $this->t('- None -'),
       '#default_value' => $webform->get('category'),
     ];
+    $form['status'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Status'),
+      '#default_value' => $webform->get('status'),
+      '#options' => [
+        WebformInterface::STATUS_OPEN => $this->t('Open'),
+        WebformInterface::STATUS_CLOSED => $this->t('Closed'),
+      ],
+      '#options_display' => 'side_by_side',
+    ];
+
     $form = $this->protectBundleIdElement($form);
 
     return parent::form($form, $form_state);
@@ -134,7 +145,7 @@ public function save(array $form, FormStateInterface $form_state) {
     ];
     $t_args = ['%label' => $webform->label()];
     $this->logger('webform')->notice('Webform @label created.', $context);
-    drupal_set_message($this->t('Webform %label created.', $t_args));
+    $this->messenger()->addStatus($this->t('Webform %label created.', $t_args));
   }
 
 }
diff --git a/web/modules/webform/src/WebformEntityDeleteForm.php b/web/modules/webform/src/WebformEntityDeleteForm.php
index 7bb560e276fbb52b6c1bdde75784f1387339a079..3f07353eeb681dbd8af3520c91c0eb0b0403cef3 100644
--- a/web/modules/webform/src/WebformEntityDeleteForm.php
+++ b/web/modules/webform/src/WebformEntityDeleteForm.php
@@ -2,38 +2,30 @@
 
 namespace Drupal\webform;
 
-use Drupal\Core\Entity\EntityDeleteForm;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Url;
-use Drupal\webform\Form\WebformDialogFormTrait;
+use Drupal\webform\Form\WebformConfigEntityDeleteFormBase;
 
 /**
  * Provides a delete webform form.
  */
-class WebformEntityDeleteForm extends EntityDeleteForm {
-
-  use WebformDialogFormTrait;
+class WebformEntityDeleteForm extends WebformConfigEntityDeleteFormBase {
 
   /**
    * {@inheritdoc}
    */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $form = parent::buildForm($form, $form_state);
-    $form['confirm'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Yes, I want to delete this webform.'),
-      '#required' => TRUE,
-      '#weight' => 10,
+  public function getDescription() {
+    return [
+      'title' => [
+        '#markup' => $this->t('This action will…'),
+      ],
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => [
+          $this->t('Remove configuration'),
+          $this->t('Delete all related submissions'),
+          $this->t('Affect any fields or nodes which reference this webform'),
+        ],
+      ],
     ];
-
-    return $this->buildDialogConfirmForm($form, $form_state);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getRedirectUrl() {
-    return Url::fromRoute('entity.webform.collection');
   }
 
 }
diff --git a/web/modules/webform/src/WebformEntityElementsForm.php b/web/modules/webform/src/WebformEntityElementsForm.php
index 38de680b3db94568ade11640803a9da92364eb0c..1fde59422fc32bc64a4d99b73134efdccd7a8167 100644
--- a/web/modules/webform/src/WebformEntityElementsForm.php
+++ b/web/modules/webform/src/WebformEntityElementsForm.php
@@ -110,7 +110,7 @@ public function form(array $form, FormStateInterface $form_state) {
       '#element_validate' => ['::validateElementsYaml'],
     ];
 
-    $form['token_tree_link'] = $this->tokenManager->buildTreeLink();
+    $form['token_tree_link'] = $this->tokenManager->buildTreeElement();
 
     $this->tokenManager->elementValidate($form);
 
@@ -159,7 +159,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
     if ($messages = $this->elementsValidator->validate($webform)) {
       $form_state->setErrorByName('elements');
       foreach ($messages as $message) {
-        drupal_set_message($message, 'error');
+        $this->messenger()->addError($message);
       }
     }
   }
@@ -179,7 +179,7 @@ public function save(array $form, FormStateInterface $form_state) {
     ];
     $t_args = ['%label' => $webform->label()];
     $this->logger('webform')->notice('Webform @label elements saved.', $context);
-    drupal_set_message($this->t('Webform %label elements saved.', $t_args));
+    $this->messenger()->addStatus($this->t('Webform %label elements saved.', $t_args));
   }
 
   /****************************************************************************/
@@ -199,7 +199,7 @@ protected function getElementsWithoutWebformTypePrefix($value) {
     }
 
     $this->removeWebformTypePrefixRecursive($elements);
-    return WebformYaml::tidy(Yaml::encode($elements));
+    return WebformYaml::encode($elements);
   }
 
   /**
@@ -236,7 +236,7 @@ protected function getElementsWithWebformTypePrefix($value) {
     }
 
     $this->addWebformTypePrefixRecursive($elements);
-    return WebformYaml::tidy(Yaml::encode($elements));
+    return WebformYaml::encode($elements);
   }
 
   /**
diff --git a/web/modules/webform/src/WebformEntityElementsValidator.php b/web/modules/webform/src/WebformEntityElementsValidator.php
index 94deef795dac73b5853239781438e07f9b33d92e..294bf5861f03c30f8089431e2e2e59294cccfe17 100644
--- a/web/modules/webform/src/WebformEntityElementsValidator.php
+++ b/web/modules/webform/src/WebformEntityElementsValidator.php
@@ -6,7 +6,6 @@
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Serialization\Yaml;
-use Drupal\Core\Render\Element;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Url;
@@ -56,6 +55,13 @@ class WebformEntityElementsValidator implements WebformEntityElementsValidatorIn
    */
   protected $originalElements;
 
+  /**
+   * An array of element keys.
+   *
+   * @var array
+   */
+  protected $elementKeys;
+
   /**
    * The 'renderer' service.
    *
@@ -84,6 +90,19 @@ class WebformEntityElementsValidator implements WebformEntityElementsValidatorIn
    */
   protected $formBuilder;
 
+  /**
+   * Element keys/names that are reserved.
+   *
+   * @var array
+   */
+  public static $reservedNames = [
+    'add',
+    'form_build_id',
+    'form_id',
+    'form_token',
+    'op',
+  ];
+
   /**
    * Constructs a WebformEntityElementsValidator object.
    *
@@ -106,51 +125,73 @@ public function __construct(RendererInterface $renderer, WebformElementManagerIn
   /**
    * {@inheritdoc}
    */
-  public function validate(WebformInterface $webform) {
+  public function validate(WebformInterface $webform, array $options = []) {
+    $options += [
+      'required' => TRUE,
+      'yaml' => TRUE,
+      'array' => TRUE,
+      'names' => TRUE,
+      'properties' => TRUE,
+      'submissions' => TRUE,
+      'hierarchy' => TRUE,
+      'rendering' => TRUE,
+    ];
+
     $this->webform = $webform;
 
     $this->elementsRaw = $webform->getElementsRaw();
     $this->originalElementsRaw = $webform->getElementsOriginalRaw();
 
     // Validate required.
-    if ($message = $this->validateRequired()) {
+    if ($options['required'] && ($message = $this->validateRequired())) {
       return [$message];
     }
+
     // Validate contain valid YAML.
-    if ($message = $this->validateYaml()) {
+    if ($options['yaml'] && ($message = $this->validateYaml())) {
       return [$message];
     }
 
     $this->elements = Yaml::decode($this->elementsRaw);
     $this->originalElements = Yaml::decode($this->originalElementsRaw);
 
+    $this->elementKeys = [];
+    if (is_array($this->elements)) {
+      $this->getElementKeysRecursive($this->elements, $this->elementKeys);
+    }
+
     // Validate elements are an array.
-    if ($message = $this->validateArray()) {
+    if ($options['array'] && ($message = $this->validateArray())) {
       return [$message];
     }
 
     // Validate duplicate element name.
-    if ($messages = $this->validateDuplicateNames()) {
-      return $messages;
+    if ($options['names']) {
+      if ($messages = $this->validateNames()) {
+        return $messages;
+      }
+      elseif ($messages = $this->validateDuplicateNames()) {
+        return $messages;
+      }
     }
 
     // Validate ignored properties.
-    if ($messages = $this->validateProperties()) {
+    if ($options['properties'] && ($messages = $this->validateProperties())) {
       return $messages;
     }
 
     // Validate submission data.
-    if ($messages = $this->validateSubmissions()) {
+    if ($options['submissions'] && ($messages = $this->validateSubmissions())) {
       return $messages;
     }
 
     // Validate hierarchy.
-    if ($messages = $this->validateHierarchy()) {
+    if ($options['hierarchy'] && ($messages = $this->validateHierarchy())) {
       return $messages;
     }
 
     // Validate rendering.
-    if ($message = $this->validateRendering()) {
+    if ($options['rendering'] && ($message = $this->validateRendering())) {
       return [$message];
     }
 
@@ -196,6 +237,35 @@ protected function validateArray() {
     return NULL;
   }
 
+  /**
+   * Validate elements names.
+   *
+   * @return array|null
+   *   If not valid, an array of error messages.
+   */
+  protected function validateNames() {
+    $messages = [];
+    foreach ($this->elementKeys as $name) {
+      if (!preg_match('/^[_a-z0-9]+$/', $name)) {
+        $line_numbers = $this->getLineNumbers('/^\s*(["\']?)' . preg_quote($name, '/') . '\1\s*:/');
+        $t_args = [
+          '%name' => $name,
+          '@line_number' => WebformArrayHelper::toString($line_numbers),
+        ];
+        $messages[] = $this->t('The element key %name on line @line_number must contain only lowercase letters, numbers, and underscores.', $t_args);
+      }
+      elseif (in_array($name, static::$reservedNames)) {
+        $line_numbers = $this->getLineNumbers('/^\s*(["\']?)' . preg_quote($name, '/') . '\1\s*:/');
+        $t_args = [
+          '%name' => $name,
+          '@line_number' => WebformArrayHelper::toString($line_numbers),
+        ];
+        $messages[] = $this->t('The element key %name on line @line_number is a reserved key.', $t_args);
+      }
+    }
+    return $messages;
+  }
+
   /**
    * Validate elements does not contain duplicate names.
    *
@@ -235,7 +305,7 @@ protected function validateDuplicateNames() {
    */
   protected function getDuplicateNamesRecursive(array $elements, array &$names) {
     foreach ($elements as $key => &$element) {
-      if (Element::property($key) || !is_array($element)) {
+      if (!WebformElementHelper::isElement($element, $key)) {
         continue;
       }
       if (isset($element['#type'])) {
@@ -333,26 +403,6 @@ protected function validateSubmissions() {
     return NULL;
   }
 
-  /**
-   * Recurse through elements and collect an associative array of deleted element keys.
-   *
-   * @param array $elements
-   *   An array of elements.
-   * @param array $names
-   *   An array tracking deleted element keys.
-   */
-  protected function getElementKeysRecursive(array $elements, array &$names) {
-    foreach ($elements as $key => &$element) {
-      if (Element::property($key) || !is_array($element)) {
-        continue;
-      }
-      if (isset($element['#type'])) {
-        $names[$key] = $key;
-      }
-      $this->getElementKeysRecursive($element, $names);
-    }
-  }
-
   /**
    * Validate element hierarchy.
    *
@@ -419,7 +469,7 @@ protected function validateRendering() {
     catch (\Exception $exception) {
       $message = $exception->getMessage();
     }
-    // Restore  Drupal's error and exception handler.
+    // Restore Drupal's error and exception handler.
     restore_error_handler();
     restore_exception_handler();
 
@@ -439,6 +489,30 @@ protected function validateRendering() {
     return $message;
   }
 
+  /****************************************************************************/
+  // Helper methods.
+  /****************************************************************************/
+
+  /**
+   * Recurse through elements and collect an associative array of deleted element keys.
+   *
+   * @param array $elements
+   *   An array of elements.
+   * @param array $names
+   *   An array tracking deleted element keys.
+   */
+  protected function getElementKeysRecursive(array $elements, array &$names) {
+    foreach ($elements as $key => &$element) {
+      if (!WebformElementHelper::isElement($element, $key)) {
+        continue;
+      }
+      if (isset($element['#type'])) {
+        $names[$key] = $key;
+      }
+      $this->getElementKeysRecursive($element, $names);
+    }
+  }
+
   /**
    * Get the line numbers for given pattern in the webform's elements string.
    *
diff --git a/web/modules/webform/src/WebformEntityElementsValidatorInterface.php b/web/modules/webform/src/WebformEntityElementsValidatorInterface.php
index 33c2b0633dbad547efbfaf2e0c6c9a6f87cd9aaa..4c935c9d0a4dde2749823a97e1f04cd785cc3f64 100644
--- a/web/modules/webform/src/WebformEntityElementsValidatorInterface.php
+++ b/web/modules/webform/src/WebformEntityElementsValidatorInterface.php
@@ -12,10 +12,12 @@ interface WebformEntityElementsValidatorInterface {
    *
    * @param \Drupal\webform\WebformInterface $webform
    *   A webform.
+   * @param array $options
+   *   An array of validation rules to check.
    *
    * @return array|null
    *   An array of error messages or NULL if the elements are valid.
    */
-  public function validate(WebformInterface $webform);
+  public function validate(WebformInterface $webform, array $options = []);
 
 }
diff --git a/web/modules/webform/modules/webform_devel/src/Form/WebformDevelEntityExportForm.php b/web/modules/webform/src/WebformEntityExportForm.php
similarity index 95%
rename from web/modules/webform/modules/webform_devel/src/Form/WebformDevelEntityExportForm.php
rename to web/modules/webform/src/WebformEntityExportForm.php
index 3e5775eab1fdbde78500b4519fbad5f4daefc66f..577f5f56ab25fa5fbbca68c73c47dfa86418ebb5 100644
--- a/web/modules/webform/modules/webform_devel/src/Form/WebformDevelEntityExportForm.php
+++ b/web/modules/webform/src/WebformEntityExportForm.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\webform_devel\Form;
+namespace Drupal\webform;
 
 use Drupal\Core\Serialization\Yaml;
 use Drupal\Core\Entity\EntityForm;
@@ -11,7 +11,7 @@
 /**
  * Export webform configuration.
  */
-class WebformDevelEntityExportForm extends EntityForm {
+class WebformEntityExportForm extends EntityForm {
 
   /**
    * {@inheritdoc}
diff --git a/web/modules/webform/src/WebformEntityHandlersForm.php b/web/modules/webform/src/WebformEntityHandlersForm.php
index 7d1e74f3c3555d081ae9e45156699ca29679237e..4b3e4c4c625465f72cecf2186a9ec96072086a0b 100644
--- a/web/modules/webform/src/WebformEntityHandlersForm.php
+++ b/web/modules/webform/src/WebformEntityHandlersForm.php
@@ -2,14 +2,18 @@
 
 namespace Drupal\webform;
 
+use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
+use Drupal\webform\Ajax\WebformRefreshCommand;
 use Drupal\webform\Form\WebformEntityAjaxFormTrait;
 use Drupal\webform\Plugin\WebformHandlerInterface;
 use Drupal\webform\Plugin\WebformHandlerManagerInterface;
 use Drupal\webform\Utility\WebformDialogHelper;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Provides a webform to manage submission handlers.
@@ -55,18 +59,18 @@ public static function create(ContainerInterface $container) {
    * {@inheritdoc}
    */
   public function form(array $form, FormStateInterface $form_state) {
-    /** @var \Drupal\webform\WebformInterface $webform */
-    $webform = $this->getEntity();
-
     $user_input = $form_state->getUserInput();
 
+    // Hard code the form id.
+    $form['#id'] = 'webform-handlers-form';
+
     // Build table header.
     $header = [
       ['data' => $this->t('Title / Description')],
       ['data' => $this->t('ID'), 'class' => [RESPONSIVE_PRIORITY_LOW]],
       ['data' => $this->t('Summary'), 'class' => [RESPONSIVE_PRIORITY_LOW]],
       ['data' => $this->t('Status'), 'class' => [RESPONSIVE_PRIORITY_LOW]],
-      ['data' => $this->t('Weight'), 'class' => [RESPONSIVE_PRIORITY_LOW]],
+      ['data' => $this->t('Weight'), 'class' => ['webform-tabledrag-hide']],
       ['data' => $this->t('Operations')],
     ];
 
@@ -121,9 +125,11 @@ public function form(array $form, FormStateInterface $form_state) {
         '#attributes' => [
           'class' => ['webform-handler-order-weight'],
         ],
+        '#wrapper_attributes' => ['class' => ['webform-tabledrag-hide']],
       ];
 
       $operations = [];
+      // Edit.
       $operations['edit'] = [
         'title' => $this->t('Edit'),
         'url' => Url::fromRoute('entity.webform.handler.edit_form', [
@@ -132,6 +138,7 @@ public function form(array $form, FormStateInterface $form_state) {
         ]),
         'attributes' => WebformDialogHelper::getOffCanvasDialogAttributes(),
       ];
+      // Duplicate.
       if ($handler->cardinality() === WebformHandlerInterface::CARDINALITY_UNLIMITED) {
         $operations['duplicate'] = [
           'title' => $this->t('Duplicate'),
@@ -142,6 +149,27 @@ public function form(array $form, FormStateInterface $form_state) {
           'attributes' => WebformDialogHelper::getOffCanvasDialogAttributes(),
         ];
       }
+      // Test individual handler.
+      if ($this->entity->access('test')) {
+        $operations['test'] = [
+          'title' => $this->t('Test'),
+          'url' => Url::fromRoute(
+            'entity.webform.test_form',
+            ['webform' => $this->entity->id()],
+            ['query' => ['_webform_handler' => $handler_id]]
+          ),
+        ];
+      }
+      // Add AJAX functionality to enable/disable operations.
+      $operations['status'] = [
+        'title' => $handler->isEnabled() ? $this->t('Disable') : $this->t('Enable'),
+        'url' => Url::fromRoute('entity.webform.handler.' . ($handler->isEnabled() ? 'disable' : 'enable'), [
+          'webform' => $this->entity->id(),
+          'webform_handler' => $handler_id,
+        ]),
+        'attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW, ['use-ajax']),
+      ];
+      // Delete.
       $operations['delete'] = [
         'title' => $this->t('Delete'),
         'url' => Url::fromRoute('entity.webform.handler.delete_form', [
@@ -150,6 +178,7 @@ public function form(array $form, FormStateInterface $form_state) {
         ]),
         'attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW),
       ];
+
       $row['operations'] = [
         '#type' => 'operations',
         '#links' => $operations,
@@ -160,41 +189,6 @@ public function form(array $form, FormStateInterface $form_state) {
       $rows[$handler_id] = $row;
     }
 
-    // Filter add handler by excluded_handlers.
-    $handler_definitions = $this->handlerManager->getDefinitions();
-    $handler_definitions = $this->handlerManager->removeExcludeDefinitions($handler_definitions);
-    unset($handler_definitions['broken']);
-
-    // Must manually add local actions to the webform because we can't alter local
-    // actions and add the needed dialog attributes.
-    // @see https://www.drupal.org/node/2585169
-    $local_actions = [];
-    if (isset($handler_definitions['email'])) {
-      $local_actions['add_email'] = [
-        '#theme' => 'menu_local_action',
-        '#link' => [
-          'title' => $this->t('Add email'),
-          'url' => new Url('entity.webform.handler.add_form', ['webform' => $webform->id(), 'webform_handler' => 'email']),
-          'attributes' => WebformDialogHelper::getOffCanvasDialogAttributes(),
-        ],
-      ];
-    }
-    unset($handler_definitions['email']);
-    if ($handler_definitions) {
-      $local_actions['add_handler'] = [
-        '#theme' => 'menu_local_action',
-        '#link' => [
-          'title' => $this->t('Add handler'),
-          'url' => new Url('entity.webform.handler', ['webform' => $webform->id()]),
-          'attributes' => WebformDialogHelper::getModalDialogAttributes(),
-        ],
-      ];
-    }
-    $form['local_actions'] = [
-      '#prefix' => '<ul class="action-links">',
-      '#suffix' => '</ul>',
-    ] + $local_actions;
-
     // Build the list of existing webform handlers for this webform.
     $form['handlers'] = [
       '#type' => 'table',
@@ -208,12 +202,14 @@ public function form(array $form, FormStateInterface $form_state) {
       ],
       '#attributes' => [
         'id' => 'webform-handlers',
+        'class' => ['webform-handlers-table'],
       ],
       '#empty' => $this->t('There are currently no handlers setup for this webform.'),
     ] + $rows;
 
     // Must preload libraries required by (modal) dialogs.
     WebformDialogHelper::attachLibraries($form);
+    $form['#attached']['library'][] = 'webform/webform.admin.tabledrag';
 
     return parent::form($form, $form_state);
   }
@@ -254,7 +250,7 @@ public function save(array $form, FormStateInterface $form_state) {
     ];
     $this->logger('webform')->notice('Webform @label handler saved.', $context);
 
-    drupal_set_message($this->t('Webform %label handler saved.', ['%label' => $webform->label()]));
+    $this->messenger()->addStatus($this->t('Webform %label handler saved.', ['%label' => $webform->label()]));
   }
 
   /**
@@ -272,4 +268,48 @@ protected function updateHandlerWeights(array $handlers) {
     }
   }
 
+  /**
+   * Calls a method on a webform handler and reloads the webform handlers form.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   The webform being acted upon.
+   * @param string $webform_handler
+   *   THe webform handler id.
+   * @param string $op
+   *   The operation to perform, e.g., 'enable' or 'disable'.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse|\Symfony\Component\HttpFoundation\RedirectResponse
+   *   Either returns an AJAX response that refreshes the webform's handlers
+   *   page, or redirects back to the webform's handlers page.
+   */
+  public static function ajaxOperation(WebformInterface $webform, $webform_handler, $op, Request $request) {
+    // Perform the handler disable/enable operation.
+    $handler = $webform->getHandler($webform_handler);
+    $handler->$op();
+    // Save the webform.
+    $webform->save();
+
+    // Display message.
+    $t_args = [
+      '@label' => $handler->label(),
+      '@op' => ($op === 'enable') ? t('enabled') : t('disabled'),
+    ];
+    \Drupal::messenger()->addStatus(t('This @label handler was @op.', $t_args));
+
+    // Get the webform's handlers form URL.
+    $url = $webform->toUrl('handlers', ['query' => ['update' => $webform_handler]])->toString();
+
+    // If the request is via AJAX, return the webform handlers form.
+    if ($request->request->get('js')) {
+      $response = new AjaxResponse();
+      $response->addCommand(new WebformRefreshCommand($url));
+      return $response;
+    }
+
+    // Otherwise, redirect back to the webform handlers form.
+    return new RedirectResponse($url);
+  }
+
 }
diff --git a/web/modules/webform/src/WebformEntityListBuilder.php b/web/modules/webform/src/WebformEntityListBuilder.php
index 4de89d676b53e53019a4d3ef5c123186a76a5f18..2499c0947266cab2a50345cfb20799dae9b950ad 100644
--- a/web/modules/webform/src/WebformEntityListBuilder.php
+++ b/web/modules/webform/src/WebformEntityListBuilder.php
@@ -6,10 +6,14 @@
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
 use Drupal\webform\Element\WebformHtmlEditor;
 use Drupal\webform\Utility\WebformDialogHelper;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Defines a class to build a listing of webform entities.
@@ -18,6 +22,20 @@
  */
 class WebformEntityListBuilder extends ConfigEntityListBuilder {
 
+  /**
+   * The current request.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
   /**
    * Search keys.
    *
@@ -61,24 +79,43 @@ class WebformEntityListBuilder extends ConfigEntityListBuilder {
   protected $roleStorage;
 
   /**
-   * Associative array container total results for all webforms.
+   * Constructs a new WebformListBuilder object.
    *
-   * @var array
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   *   The entity storage class.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
    */
-  protected $resultsTotals;
+  public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, RequestStack $request_stack, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager) {
+    parent::__construct($entity_type, $storage);
+    $this->request = $request_stack->getCurrentRequest();
+    $this->currentUser = $current_user;
+
+    $this->keys = $this->request->query->get('search');
+    $this->category = $this->request->query->get('category');
+    $this->state = $this->request->query->get('state');
+    $this->submissionStorage = $entity_type_manager->getStorage('webform_submission');
+    $this->userStorage = $entity_type_manager->getStorage('user');
+    $this->roleStorage = $entity_type_manager->getStorage('user_role');
+  }
 
   /**
    * {@inheritdoc}
    */
-  public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage) {
-    parent::__construct($entity_type, $storage);
-
-    $this->keys = \Drupal::request()->query->get('search');
-    $this->category = \Drupal::request()->query->get('category');
-    $this->state = \Drupal::request()->query->get('state');
-    $this->submissionStorage = \Drupal::entityTypeManager()->getStorage('webform_submission');
-    $this->userStorage = \Drupal::entityTypeManager()->getStorage('user');
-    $this->roleStorage = \Drupal::entityTypeManager()->getStorage('user_role');
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('entity.manager')->getStorage($entity_type->id()),
+      $container->get('request_stack'),
+      $container->get('current_user'),
+      $container->get('entity_type.manager')
+    );
   }
 
   /**
@@ -94,56 +131,79 @@ public function render() {
 
     $build = [];
 
-    // Must manually add local actions to the webform because we can't alter local
-    // actions and add the needed dialog attributes.
-    // @see https://www.drupal.org/node/2585169
-    if (\Drupal::currentUser()->hasPermission('create webform')) {
-      $build['local_actions'] = [
-        'add_form' => [
-          '#type' => 'link',
-          '#title' => $this->t('Add webform'),
-          '#url' => new Url('entity.webform.add_form'),
-          '#attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW, ['button', 'button-action', 'button--primary', 'button--small']),
-        ],
-      ];
-    }
+    // Filter form.
+    $build['filter_form'] = $this->buildFilterForm();
 
+    // Display info.
+    $build['info'] = $this->buildInfo();
+
+    // Table.
+    $build += parent::render();
+    $build['table']['#sticky'] = TRUE;
+    $build['table']['#attributes']['class'][] = 'webform-forms';
+
+    // Attachments.
+    // Must preload libraries required by (modal) dialogs.
+    WebformDialogHelper::attachLibraries($build);
+
+    return $build;
+  }
+
+  /**
+   * Build the filter form.
+   *
+   * @return array
+   *   A render array representing the filter form.
+   */
+  protected function buildFilterForm() {
     // Add the filter by key(word) and/or state.
-    if (\Drupal::currentUser()->hasPermission('administer webform')) {
+    if ($this->currentUser->hasPermission('administer webform')) {
       $state_options = [
-        '' => $this->t('All [@total]', ['@total' => $this->getTotal(NULL, NULL)]),
-        WebformInterface::STATUS_OPEN => $this->t('Open [@total]', ['@total' => $this->getTotal(NULL, NULL, WebformInterface::STATUS_OPEN)]),
-        WebformInterface::STATUS_CLOSED => $this->t('Closed [@total]', ['@total' => $this->getTotal(NULL, NULL, WebformInterface::STATUS_CLOSED)]),
-        WebformInterface::STATUS_SCHEDULED => $this->t('Scheduled [@total]', ['@total' => $this->getTotal(NULL, NULL, WebformInterface::STATUS_SCHEDULED)]),
+        (string) $this->t('Active') => [
+          '' => $this->t('All [@total]', ['@total' => $this->getTotal(NULL, NULL)]),
+          WebformInterface::STATUS_OPEN => $this->t('Open [@total]', ['@total' => $this->getTotal(NULL, NULL, WebformInterface::STATUS_OPEN)]),
+          WebformInterface::STATUS_CLOSED => $this->t('Closed [@total]', ['@total' => $this->getTotal(NULL, NULL, WebformInterface::STATUS_CLOSED)]),
+          WebformInterface::STATUS_SCHEDULED => $this->t('Scheduled [@total]', ['@total' => $this->getTotal(NULL, NULL, WebformInterface::STATUS_SCHEDULED)]),
+        ],
+        (string) $this->t('Inactive') => [
+          WebformInterface::STATUS_ARCHIVED => $this->t('Archived [@total]', ['@total' => $this->getTotal(NULL, NULL, WebformInterface::STATUS_ARCHIVED)]),
+        ],
       ];
     }
     else {
       $state_options = [
-        '' => $this->t('All'),
-        WebformInterface::STATUS_OPEN => $this->t('Open'),
-        WebformInterface::STATUS_CLOSED => $this->t('Closed'),
-        WebformInterface::STATUS_SCHEDULED => $this->t('Scheduled'),
+        (string) $this->t('Active') => [
+          '' => $this->t('All'),
+          WebformInterface::STATUS_OPEN => $this->t('Open'),
+          WebformInterface::STATUS_CLOSED => $this->t('Closed'),
+          WebformInterface::STATUS_SCHEDULED => $this->t('Scheduled'),
+        ],
+        (string) $this->t('Inactive') => [
+          WebformInterface::STATUS_ARCHIVED => $this->t('Archived'),
+        ],
       ];
     }
-    $build['filter_form'] = \Drupal::formBuilder()->getForm('\Drupal\webform\Form\WebformEntityFilterForm', $this->keys, $this->category, $this->state, $state_options);
+    return \Drupal::formBuilder()->getForm('\Drupal\webform\Form\WebformEntityFilterForm', $this->keys, $this->category, $this->state, $state_options);
+  }
 
+  /**
+   * Build information summary.
+   *
+   * @return array
+   *   A render array representing the information summary.
+   */
+  protected function buildInfo() {
     // Display info.
-    if (\Drupal::currentUser()->hasPermission('administer webform') && ($total = $this->getTotal($this->keys, $this->category, $this->state))) {
-      $build['info'] = [
+    if ($this->currentUser->hasPermission('administer webform') && ($total = $this->getTotal($this->keys, $this->category, $this->state))) {
+      return [
         '#markup' => $this->formatPlural($total, '@total webform', '@total webforms', ['@total' => $total]),
         '#prefix' => '<div>',
         '#suffix' => '</div>',
       ];
     }
-
-    $build += parent::render();
-
-    $build['table']['#attributes']['class'][] = 'webform-forms';
-
-    // Must preload libraries required by (modal) dialogs.
-    WebformDialogHelper::attachLibraries($build);
-
-    return $build;
+    else {
+      return [];
+    }
   }
 
   /**
@@ -180,18 +240,15 @@ public function buildHeader() {
       'specifier' => 'uid',
       'field' => 'uid',
     ];
-    $header['results_total'] = [
-      'data' => $this->t('Total Results'),
+    $header['results'] = [
+      'data' => $this->t('Results'),
       'class' => [RESPONSIVE_PRIORITY_MEDIUM],
-      'specifier' => 'results_total',
-      'field' => 'results_total',
+      'specifier' => 'results',
+      'field' => 'results',
     ];
-    $header['results_operations'] = [
+    $header['operations'] = [
       'data' => $this->t('Operations'),
-      'class' => [RESPONSIVE_PRIORITY_MEDIUM],
     ];
-    $header['operations'] = '';
-
     return $header;
   }
 
@@ -200,8 +257,9 @@ public function buildHeader() {
    */
   public function buildRow(EntityInterface $entity) {
     /* @var $entity \Drupal\webform\WebformInterface */
-    $settings = $entity->getSettings();
 
+    // Title.
+    //
     // ISSUE: Webforms that the current user can't access are not being hidden via the EntityQuery.
     // WORK-AROUND: Don't link to the webform.
     // See: Access control is not applied to config entity queries
@@ -210,29 +268,85 @@ public function buildRow(EntityInterface $entity) {
     if ($entity->isTemplate()) {
       $row['title']['data']['template'] = ['#markup' => ' <b>(' . $this->t('Template') . ')</b>'];
     }
+
+    // Description.
     $row['description']['data'] = WebformHtmlEditor::checkMarkup($entity->get('description'));
+
+    // Category.
     $row['category']['data']['#markup'] = $entity->get('category');
-    switch ($entity->get('status')) {
-      case WebformInterface::STATUS_OPEN:
-        $row['status'] = $this->t('Open');
-        break;
 
-      case WebformInterface::STATUS_CLOSED:
-        $row['status'] = $this->t('Closed');
-        break;
+    // Status.
+    $t_args = ['@label' => $entity->label()];
+    if ($entity->isArchived()) {
+      $row['status']['data'] = [
+        '#type' => 'html_tag',
+        '#tag' => 'span',
+        '#markup' => $this->t('Archived'),
+        '#attributes' => ['aria-label' => $this->t('@label is archived', $t_args)],
+      ];
+      $row['status'] = $this->t('Archived');
+    }
+    else {
+      switch ($entity->get('status')) {
+        case WebformInterface::STATUS_OPEN:
+          $status = $this->t('Open');
+          $aria_label = $this->t('@label is open', $t_args);
+          break;
+
+        case WebformInterface::STATUS_CLOSED:
+          $status = $this->t('Closed');
+          $aria_label = $this->t('@label is closed', $t_args);
+          break;
+
+        case WebformInterface::STATUS_SCHEDULED:
+          $status = $this->t('Scheduled (@state)', ['@state' => $entity->isOpen() ? $this->t('Open') : $this->t('Closed')]);
+          $aria_label = $this->t('@label is scheduled and is @state', $t_args + ['@state' => $entity->isOpen() ? $this->t('open') : $this->t('closed')]);
+          break;
+
+        default:
+          return [];
+      }
 
-      case WebformInterface::STATUS_SCHEDULED:
-        $row['status'] = $this->t('Scheduled (@state)', ['@state' => $entity->isOpen() ? $this->t('Open') : $this->t('Closed')]);
-        break;
+      if ($entity->access('update')) {
+        $row['status']['data'] = $entity->toLink($status, 'settings-form', ['query' => $this->getDestinationArray()])->toRenderable() + [
+          '#attributes' => ['aria-label' => $aria_label],
+        ];
+      }
+      else {
+        $row['status']['data'] = [
+          '#type' => 'html_tag',
+          '#tag' => 'span',
+          '#markup' => $status,
+          '#attributes' => ['aria-label' => $aria_label],
+        ];
+      }
     }
+
+    // Owner.
     $row['owner'] = ($owner = $entity->getOwner()) ? $owner->toLink() : '';
-    $row['results_total'] = $this->getResultsTotal($entity->id()) . ($entity->isResultsDisabled() ? ' ' . $this->t('(Disabled)') : '');
-    $row['results_operations']['data'] = [
-      '#type' => 'operations',
-      '#links' => $this->getDefaultOperations($entity, 'results'),
-      '#prefix' => '<div class="webform-dropbutton">',
-      '#suffix' => '</div>',
-    ];
+
+    // Results.
+    $result_total = $this->storage->getTotalNumberOfResults($entity->id());
+    $results_access = $entity->access('submission_view_any');
+    $results_disabled = $entity->isResultsDisabled();
+    if ($results_disabled || !$results_access) {
+      $row['results'] = $result_total . ($entity->isResultsDisabled() ? ' ' . $this->t('(Disabled)') : '');
+    }
+    else {
+      $row['results'] = [
+        'data' => [
+          '#type' => 'link',
+          '#title' => $result_total,
+          '#attributes' => [
+            'aria-label' => $this->formatPlural($result_total, '@count result for @label', '@count results for @label', ['@label' => $entity->label()]),
+          ],
+          '#url' => $entity->toUrl('results-submissions'),
+          '#suffix' => ($entity->isResultsDisabled() ? ' ' . $this->t('(Disabled)') : ''),
+        ],
+      ];
+    }
+
+    // Operations.
     return $row + parent::buildRow($entity);
   }
 
@@ -251,63 +365,45 @@ public function buildOperations(EntityInterface $entity) {
    */
   public function getDefaultOperations(EntityInterface $entity, $type = 'edit') {
     /* @var $entity \Drupal\webform\WebformInterface */
-    $route_parameters = ['webform' => $entity->id()];
-    if ($type == 'results') {
-      $operations = [];
-      if ($entity->access('submission_view_any')) {
-        $operations['submissions'] = [
-          'title' => $this->t('Submissions'),
-          'url' => Url::fromRoute('entity.webform.results_submissions', $route_parameters),
-        ];
-        $operations['export'] = [
-          'title' => $this->t('Download'),
-          'url' => Url::fromRoute('entity.webform.results_export', $route_parameters),
-        ];
-      }
-      if ($entity->access('submission_delete_any')) {
-        $operations['clear'] = [
-          'title' => $this->t('Clear'),
-          'url' => Url::fromRoute('entity.webform.results_clear', $route_parameters),
-        ];
-      }
+
+    $operations = [];
+    if ($entity->access('update')) {
+      $operations['edit'] = [
+        'title' => $this->t('Build'),
+        'url' => $this->ensureDestination($entity->toUrl('edit-form')),
+      ];
     }
-    else {
-      $operations = parent::getDefaultOperations($entity);
-      if (isset($operations['edit'])) {
-        $operations['edit']['title'] = $this->t('Build');
-      }
-      if ($entity->access('update')) {
-        $operations['settings'] = [
-          'title' => $this->t('Settings'),
-          'weight' => 22,
-          'url' => Url::fromRoute('entity.webform.settings', $route_parameters),
-        ];
-      }
-      if ($entity->access('submission_page')) {
-        $operations['view'] = [
-          'title' => $this->t('View'),
-          'weight' => 24,
-          'url' => Url::fromRoute('entity.webform.canonical', $route_parameters),
-        ];
-      }
-      if ($entity->access('submission_update_any')) {
-        $operations['test'] = [
-          'title' => $this->t('Test'),
-          'weight' => 25,
-          'url' => Url::fromRoute('entity.webform.test_form', $route_parameters),
-        ];
-      }
-      if ($entity->access('duplicate')) {
-        $operations['duplicate'] = [
-          'title' => $this->t('Duplicate'),
-          'weight' => 26,
-          'url' => Url::fromRoute('entity.webform.duplicate_form', $route_parameters),
-          'attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW),
-        ];
-      }
-      if (isset($operations['delete'])) {
-        $operations['delete']['attributes'] = WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW);
-      }
+    if ($entity->access('submission_page')) {
+      $operations['view'] = [
+        'title' => $this->t('View'),
+        'url' => $entity->toUrl('canonical'),
+      ];
+    }
+    if ($entity->access('submission_view_any') && !$entity->isResultsDisabled()) {
+      $operations['results'] = [
+        'title' => $this->t('Results'),
+        'url' => $entity->toUrl('results-submissions'),
+      ];
+    }
+    if ($entity->access('update')) {
+      $operations['settings'] = [
+        'title' => $this->t('Settings'),
+        'url' => $entity->toUrl('settings'),
+      ];
+    }
+    if ($entity->access('duplicate')) {
+      $operations['duplicate'] = [
+        'title' => $this->t('Duplicate'),
+        'url' => $entity->toUrl('duplicate-form'),
+        'attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW),
+      ];
+    }
+    if ($entity->access('delete') && $entity->hasLinkTemplate('delete-form')) {
+      $operations['delete'] = [
+        'title' => $this->t('Delete'),
+        'url' => $this->ensureDestination($entity->toUrl('delete-form')),
+        'attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW),
+      ];
     }
     return $operations;
   }
@@ -317,17 +413,17 @@ public function getDefaultOperations(EntityInterface $entity, $type = 'edit') {
    */
   protected function getEntityIds() {
     $header = $this->buildHeader();
-    if (\Drupal::request()->query->get('order') === (string) $header['results_total']['data']) {
+    if ($this->request->query->get('order') === (string) $header['results']['data']) {
       // Get results totals for all returned entity ids.
       $results_totals = $this->getQuery($this->keys, $this->category, $this->state)
         ->execute();
       foreach ($results_totals as $entity_id) {
-        $results_totals[$entity_id] = $this->getResultsTotal($entity_id);
+        $results_totals[$entity_id] = $this->storage->getTotalNumberOfResults($entity_id);
       }
 
       // Sort results totals.
       asort($results_totals, SORT_NUMERIC);
-      if (\Drupal::request()->query->get('sort') === 'desc') {
+      if ($this->request->query->get('sort') === 'desc') {
         $results_totals = array_reverse($results_totals, TRUE);
       }
 
@@ -336,7 +432,7 @@ protected function getEntityIds() {
       $entity_ids = array_combine($entity_ids, $entity_ids);
 
       // Manually initialize and apply paging to the entity ids.
-      $page = \Drupal::request()->query->get('page') ?: 0;
+      $page = $this->request->query->get('page') ?: 0;
       $total = count($entity_ids);
       $limit = $this->getLimit();
       $start = ($page * $limit);
@@ -351,41 +447,6 @@ protected function getEntityIds() {
     }
   }
 
-  /**
-   * Get total number of results for a webform.
-   *
-   * @param string $webform_id
-   *   A webform id.
-   *
-   * @return int
-   *   Total number of results for a webform.
-   */
-  protected function getResultsTotal($webform_id) {
-    $results_totals = $this->getResultsTotals();
-    return (isset($results_totals[$webform_id])) ? $results_totals[$webform_id] : 0;
-  }
-
-  /**
-   * Get total results for all webforms.
-   *
-   * @return array
-   *   An associative array keyed by webform id contains total results for
-   *   all webforms.
-   */
-  protected function getResultsTotals() {
-    if (isset($this->resultsTotals)) {
-      return $this->resultsTotals;
-    }
-
-    $query = \Drupal::database()->select('webform_submission', 'ws');
-    $query->fields('ws', ['webform_id']);
-    $query->addExpression('COUNT(sid)', 'results_total');
-    $query->groupBy('webform_id');
-
-    $this->resultsTotals = array_map('intval', $query->execute()->fetchAllKeyed());
-    return $this->resultsTotals;
-  }
-
   /**
    * Get the total number of submissions.
    *
@@ -472,12 +533,19 @@ protected function getQuery($keys = '', $category = '', $state = '') {
     }
 
     // Filter by (form) state.
-    if ($state) {
-      $query->condition('status', $state);
+    switch ($state) {
+      case WebformInterface::STATUS_OPEN;
+      case WebformInterface::STATUS_CLOSED;
+      case WebformInterface::STATUS_SCHEDULED;
+        $query->condition('status', $state);
+        break;
     }
 
+    // Always filter by archived state.
+    $query->condition('archive', $state === WebformInterface::STATUS_ARCHIVED ? 1 : 0);
+
     // Filter out templates if the webform_template.module is enabled.
-    if ($this->moduleHandler()->moduleExists('webform_templates')) {
+    if ($this->moduleHandler()->moduleExists('webform_templates') && $state !== WebformInterface::STATUS_ARCHIVED) {
       $query->condition('template', FALSE);
     }
     return $query;
@@ -521,7 +589,7 @@ protected function getLimit() {
    *   permission.
    */
   protected function isAdmin() {
-    $account = \Drupal::currentUser();
+    $account = $this->currentUser;
     return ($account->hasPermission('administer webform') || $account->hasPermission('edit any webform') || $account->hasPermission('view any webform submission'));
   }
 
@@ -529,6 +597,7 @@ protected function isAdmin() {
    * {@inheritdoc}
    */
   protected function ensureDestination(Url $url) {
+    // Never add add a destination to operation URLs.
     return $url;
   }
 
diff --git a/web/modules/webform/src/WebformEntityReferenceManager.php b/web/modules/webform/src/WebformEntityReferenceManager.php
index bda4fc01e07bea26898155afd64fcacc951602c5..8c81048e634f965549af8432fd62b5b0922b2509 100644
--- a/web/modules/webform/src/WebformEntityReferenceManager.php
+++ b/web/modules/webform/src/WebformEntityReferenceManager.php
@@ -14,7 +14,7 @@
  * Webform entity reference (field) manager.
  *
  * The webform entity reference (field) manager is used to track webforms that
- * are attached to entities, specifically webform nodes.  Generally, only one
+ * are attached to entities, specifically webform nodes. Generally, only one
  * webform is attached to a single node. Field API does allow multiple
  * webforms to be attached to any entity and this services helps handle this
  * edge case.
@@ -42,6 +42,20 @@ class WebformEntityReferenceManager implements WebformEntityReferenceManagerInte
    */
   protected $userData;
 
+  /**
+   * Cache of source entity webforms.
+   *
+   * @var array
+   */
+  protected $webforms = [];
+
+  /**
+   * Cache of source entity field names.
+   *
+   * @var array
+   */
+  protected $fieldNames = [];
+
   /**
    * Constructs a WebformEntityReferenceManager object.
    *
@@ -127,6 +141,21 @@ public function deleteUserWebformId(EntityInterface $entity) {
   // Field methods.
   /****************************************************************************/
 
+  /**
+   * {@inheritdoc}
+   */
+  public function hasField(EntityInterface $entity = NULL) {
+    return $this->getFieldName($entity) ? TRUE : FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldName(EntityInterface $entity = NULL) {
+    $field_names = $this->getFieldNames($entity);
+    return $field_names ? reset($field_names) : '';
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -135,6 +164,12 @@ public function getFieldNames(EntityInterface $entity = NULL) {
       return [];
     }
 
+    // Cache the source entity's field names.
+    $entity_id = $entity->getEntityTypeId() . '-' . $entity->id();
+    if (isset($this->fieldNames[$entity_id])) {
+      return $this->fieldNames[$entity_id];
+    }
+
     $field_names = [];
     if ($entity instanceof ContentEntityInterface) {
       $fields = $entity->getFieldDefinitions();
@@ -148,24 +183,10 @@ public function getFieldNames(EntityInterface $entity = NULL) {
     // Sort fields alphabetically.
     ksort($field_names);
 
+    $this->fieldNames[$entity_id] = $field_names;
     return $field_names;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function hasField(EntityInterface $entity = NULL) {
-    return $this->getFieldName($entity) ? TRUE : FALSE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFieldName(EntityInterface $entity = NULL) {
-    $field_names = $this->getFieldNames($entity);
-    return $field_names ? reset($field_names) : '';
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -173,8 +194,8 @@ public function getWebform(EntityInterface $entity = NULL) {
     if ($webform_id = $this->getUserWebformId($entity)) {
       return Webform::load($webform_id);
     }
-    elseif ($field_name = $this->getFieldName($entity)) {
-      return $entity->$field_name->entity;
+    elseif ($webforms = $this->getWebforms($entity)) {
+      return reset($webforms);
     }
     else {
       return NULL;
@@ -185,14 +206,33 @@ public function getWebform(EntityInterface $entity = NULL) {
    * {@inheritdoc}
    */
   public function getWebforms(EntityInterface $entity = NULL) {
+    // Cache the source entity's webforms.
+    $entity_id = $entity->getEntityTypeId() . '-' . $entity->id();
+    if (isset($this->webforms[$entity_id])) {
+      return $this->webforms[$entity_id];
+    }
+
     $field_names = $this->getFieldNames($entity);
     $target_entities = [];
+    $sorted_entities = [];
     foreach ($field_names as $field_name) {
       foreach ($entity->$field_name as $item) {
+        $sorted_entities[$item->target_id] = (method_exists($item->entity, 'getWeight')) ? $item->entity->getWeight() : 0;
         $target_entities[$item->target_id] = $item->entity;
       }
     }
-    return $target_entities;
+    // Sort the webforms by key and then weight.
+    ksort($sorted_entities);
+    asort($sorted_entities, SORT_NUMERIC);
+
+    // Return the sort webforms.
+    $webforms = [];
+    foreach (array_keys($sorted_entities) as $target_id) {
+      $webforms[$target_id] = $target_entities[$target_id];
+    }
+
+    $this->webforms[$entity_id] = $webforms;
+    return $webforms;
   }
 
   /****************************************************************************/
@@ -203,7 +243,6 @@ public function getWebforms(EntityInterface $entity = NULL) {
    * {@inheritdoc}
    */
   public function getTableNames() {
-    // @todo Figure out a better way to determine webform field table names.
     /** @var \Drupal\field\FieldStorageConfigInterface[] $field_storage_configs */
     $field_storage_configs = FieldStorageConfig::loadMultiple();
     $tables = [];
diff --git a/web/modules/webform/src/WebformEntityReferenceManagerInterface.php b/web/modules/webform/src/WebformEntityReferenceManagerInterface.php
index 92550880cab23691e2eb9b38f7550422ba9a9a7a..3e7490238c98d296da5fbba9bf80772202d6658b 100644
--- a/web/modules/webform/src/WebformEntityReferenceManagerInterface.php
+++ b/web/modules/webform/src/WebformEntityReferenceManagerInterface.php
@@ -70,26 +70,26 @@ public function deleteUserWebformId(EntityInterface $entity);
   public function hasField(EntityInterface $entity = NULL);
 
   /**
-   * Get an entity's webform field names.
+   * Get an entity's webform field name.
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   A fieldable content entity.
    *
-   * @return array
-   *   An array of webform fields associate with an entity.
+   * @return string
+   *   The name of the webform field or an empty string.
    */
-  public function getFieldNames(EntityInterface $entity = NULL);
+  public function getFieldName(EntityInterface $entity = NULL);
 
   /**
-   * Get an entity's webform field name.
+   * Get an entity's webform field names.
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   A fieldable content entity.
    *
-   * @return string
-   *   The name of the webform field or an empty string.
+   * @return array
+   *   An array of webform fields associate with an entity.
    */
-  public function getFieldName(EntityInterface $entity = NULL);
+  public function getFieldNames(EntityInterface $entity = NULL);
 
   /**
    * Get an entity's target webform.
diff --git a/web/modules/webform/src/WebformEntityStorage.php b/web/modules/webform/src/WebformEntityStorage.php
index ffe5704c77dbda188ce44d9076e5f67a6ebf3622..6859fba15a3dfd17194d296178ba9cdd44f5a7dd 100644
--- a/web/modules/webform/src/WebformEntityStorage.php
+++ b/web/modules/webform/src/WebformEntityStorage.php
@@ -3,11 +3,13 @@
 namespace Drupal\webform;
 
 use Drupal\Component\Uuid\UuidInterface;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Config\Entity\ConfigEntityStorage;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -24,6 +26,20 @@ class WebformEntityStorage extends ConfigEntityStorage implements WebformEntityS
    */
   protected $database;
 
+  /**
+   * The entity type manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Associative array container total results for all webforms.
+   *
+   * @var array
+   */
+  protected $totals;
+
   /**
    * Constructs a WebformEntityStorage object.
    *
@@ -37,10 +53,13 @@ class WebformEntityStorage extends ConfigEntityStorage implements WebformEntityS
    *   The language manager.
    * @param \Drupal\Core\Database\Connection $database
    *   The database connection to be used.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager service.
    */
-  public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, Connection $database) {
+  public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, Connection $database, EntityTypeManagerInterface $entity_type_manager) {
     parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager);
     $this->database = $database;
+    $this->entityTypeManager = $entity_type_manager;
   }
 
   /**
@@ -52,7 +71,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
       $container->get('config.factory'),
       $container->get('uuid'),
       $container->get('language_manager'),
-      $container->get('database')
+      $container->get('database'),
+      $container->get('entity_type.manager')
     );
   }
 
@@ -67,11 +87,26 @@ protected function doCreate(array $values) {
     // @see \Drupal\webform_ui\Form\WebformUiElementTestForm
     // @see \Drupal\webform_ui\Form\WebformUiElementTypeFormBase
     if ($id = $entity->id()) {
-      $this->entities[$id] = $entity;
+      $this->setStaticCache([$id => $entity]);
     }
     return $entity;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function doPostSave(EntityInterface $entity, $update) {
+    if ($update && $entity->getAccessRules() != $entity->original->getAccessRules()) {
+      // Invalidate webform_submission listing cache tags because due to the
+      // change in access rules of this webform, some listings might have
+      // changed for users.
+      $cache_tags = $this->entityTypeManager->getDefinition('webform_submission')->getListCacheTags();
+      Cache::invalidateTags($cache_tags);
+    }
+
+    parent::doPostSave($entity, $update);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -89,34 +124,6 @@ public function save(EntityInterface $entity) {
     return $return;
   }
 
-  /**
-   * {@inheritdoc}
-   *
-   * Config entities are not cached and there is no easy way to enable static
-   * caching. See: Issue #1885830: Enable static caching for config entities.
-   *
-   * Overriding just EntityStorageBase::load is much simpler
-   * than completely re-writting EntityStorageBase::loadMultiple. It is also
-   * worth noting that EntityStorageBase::resetCache() does purge all cached
-   * webform config entities.
-   *
-   * Webforms need to be cached when they are being loading via
-   * a webform submission, which requires a webform's elements and meta data to be
-   * initialized via Webform::initElements().
-   *
-   * @see https://www.drupal.org/node/1885830
-   * @see \Drupal\Core\Entity\EntityStorageBase::resetCache()
-   * @see \Drupal\webform\Entity\Webform::initElements()
-   */
-  public function load($id) {
-    if (isset($this->entities[$id])) {
-      return $this->entities[$id];
-    }
-
-    $this->entities[$id] = parent::load($id);
-    return $this->entities[$id];
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -132,9 +139,6 @@ public function delete(array $entities) {
     foreach ($entities as $entity) {
       $webform_ids[] = $entity->id();
     }
-    $this->database->delete('webform_submission_log')
-      ->condition('webform_id', $webform_ids, 'IN')
-      ->execute();
 
     // Delete all webform records used to track next serial.
     $this->database->delete('webform')
@@ -158,7 +162,7 @@ public function delete(array $entities) {
         }
 
         // Clear empty webform directory.
-        if (empty(file_scan_directory($file_directory, '/.*/'))) {
+        if (file_exists($file_directory) && empty(file_scan_directory($file_directory, '/.*/'))) {
           file_unmanaged_delete_recursive($file_directory);
         }
       }
@@ -187,15 +191,22 @@ public function getCategories($template = NULL) {
    * {@inheritdoc}
    */
   public function getOptions($template = NULL) {
+    /** @var \Drupal\webform\WebformInterface[] $webforms */
     $webforms = $this->loadMultiple();
     @uasort($webforms, [$this->entityType->getClass(), 'sort']);
 
     $uncategorized_options = [];
     $categorized_options = [];
     foreach ($webforms as $id => $webform) {
+      // Skip templates.
       if ($template !== NULL && $webform->get('template') != $template) {
         continue;
       }
+      // Skip archived.
+      if ($webform->isArchived()) {
+        continue;
+      }
+
       if ($category = $webform->get('category')) {
         $categorized_options[$category][$id] = $webform->label();
       }
@@ -246,7 +257,7 @@ public function setNextSerial(WebformInterface $webform, $next_serial = 1) {
    * {@inheritdoc}
    */
   public function getSerial(WebformInterface $webform) {
-    // Use a transaction with SELECT ... FOR UPDATE to lock the row between
+    // Use a transaction with SELECT … FOR UPDATE to lock the row between
     // the SELECT and the UPDATE, ensuring that multiple Webform submissions
     // at the same time do not have duplicate numbers. FOR UPDATE must be inside
     // a transaction. The return value of db_transaction() must be assigned or
@@ -288,4 +299,32 @@ public function getMaxSerial(WebformInterface $webform) {
     return $query->execute()->fetchField() + 1;
   }
 
+  /**
+   * Get total number of results for specified webform or all webforms.
+   *
+   * @param string|null $webform_id
+   *   (optional) A webform id.
+   *
+   * @return array|int
+   *   If no webform id is passed, an associative array keyed by webform id
+   *   contains total results for all webforms, otherwise the total number of
+   *   results for specified webform
+   */
+  public function getTotalNumberOfResults($webform_id = NULL) {
+    if (!isset($this->totals)) {
+      $query = $this->database->select('webform_submission', 'ws');
+      $query->fields('ws', ['webform_id']);
+      $query->addExpression('COUNT(sid)', 'results');
+      $query->groupBy('webform_id');
+      $this->totals = array_map('intval', $query->execute()->fetchAllKeyed());
+    }
+
+    if ($webform_id) {
+      return (isset($this->totals[$webform_id])) ? $this->totals[$webform_id] : 0;
+    }
+    else {
+      return $this->totals;
+    }
+  }
+
 }
diff --git a/web/modules/webform/src/WebformEntityStorageInterface.php b/web/modules/webform/src/WebformEntityStorageInterface.php
index 07f5ee6b90d1ca39241cd746d5fb4defa58eae22..a52ca1e10acaa16b7c86b2589ea4b41b602d75f7 100644
--- a/web/modules/webform/src/WebformEntityStorageInterface.php
+++ b/web/modules/webform/src/WebformEntityStorageInterface.php
@@ -73,4 +73,13 @@ public function getSerial(WebformInterface $webform);
    */
   public function getMaxSerial(WebformInterface $webform);
 
+  /**
+   * Get total results for all webforms.
+   *
+   * @return array
+   *   An associative array keyed by webform id contains total results for
+   *   all webforms.
+   */
+  public function getTotalNumberOfResults();
+
 }
diff --git a/web/modules/webform/src/WebformEntityViewBuilder.php b/web/modules/webform/src/WebformEntityViewBuilder.php
index 87300bdcaa7a9e69e9e29ab5974ab7e7810daf2a..ebb7556bf72ebbb1e24c7415bbd7fa282d517ed8 100644
--- a/web/modules/webform/src/WebformEntityViewBuilder.php
+++ b/web/modules/webform/src/WebformEntityViewBuilder.php
@@ -14,13 +14,8 @@ class WebformEntityViewBuilder extends EntityViewBuilder {
    * {@inheritdoc}
    */
   public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
-    // TODO: Determine if webform can be cached.
     /* @var $entity \Drupal\webform\WebformInterface */
-    return [
-      '#cache' => [
-        'max-age' => 0,
-      ],
-    ] + $entity->getSubmissionForm();
+    return $entity->getSubmissionForm();
   }
 
 }
diff --git a/web/modules/webform/src/WebformHelpManager.php b/web/modules/webform/src/WebformHelpManager.php
index 759f1bd50b8e8dd695e93625e2cc5d749ad5ed0c..fa706646e6b47f607cc8906f4be08bb41a2a1b3c 100644
--- a/web/modules/webform/src/WebformHelpManager.php
+++ b/web/modules/webform/src/WebformHelpManager.php
@@ -241,6 +241,8 @@ public function deleteNotification($id) {
    * {@inheritdoc}
    */
   public function buildHelp($route_name, RouteMatchInterface $route_match) {
+    $is_help_disabled = $this->configFactory->get('webform.settings')->get('ui.help_disabled');
+
     // Get path from route match.
     $path = preg_replace('/^' . preg_quote(base_path(), '/') . '/', '/', Url::fromRouteMatch($route_match)->setAbsolute(FALSE)->toString());
 
@@ -270,6 +272,11 @@ public function buildHelp($route_name, RouteMatchInterface $route_match) {
         continue;
       }
 
+      // Check is help is disabled.  Messages are always displayed.
+      if ($is_help_disabled && empty($help['message_type'])) {
+        continue;
+      }
+
       if ($help['message_type']) {
         $build[$id] = [
           '#type' => 'webform_message',
@@ -293,6 +300,10 @@ public function buildHelp($route_name, RouteMatchInterface $route_match) {
           '#info' => $help,
         ];
       }
+      // Add custom help weight.
+      if (isset($help['weight'])) {
+        $build[$id]['#weight'] = $help['weight'];
+      }
     }
 
     // Disable caching when Webform editorial module is enabled.
@@ -374,7 +385,7 @@ public function buildVideos($docs = FALSE) {
       // Content.
       $row['content'] = ['data' => []];
       $row['content']['data']['title'] = [
-        '#markup' => $video['title'],
+        '#markup' => $video['title'] . ' | ' . (isset($video['owner']) ? $video['owner'] : $this->t('Jacob Rockowitz')),
         '#prefix' => '<h3>',
         '#suffix' => '</h3>',
       ];
@@ -393,41 +404,96 @@ public function buildVideos($docs = FALSE) {
         '#prefix' => '<p>',
         '#suffix' => '</p>',
       ];
-      $row['content']['data']['resources'] = [
-        'title' => [
-          '#markup' => $this->t('Additional resources'),
-          '#prefix' => '<div><strong>',
-          '#suffix' => '</strong></div>',
-        ],
-        'links' => [
-          '#theme' => 'links',
-          '#links' => $this->getVideoLinks($id),
-          '#attributes' => ['class' => ['webform-help-links']],
-        ],
-      ];
+      if ($video_links = $this->getVideoLinks($id)) {
+        $row['content']['data']['resources'] = [
+          'title' => [
+            '#markup' => $this->t('Additional resources'),
+            '#prefix' => '<div><strong>',
+            '#suffix' => '</strong></div>',
+          ],
+          'links' => [
+            '#theme' => 'links',
+            '#links' => $video_links,
+            '#attributes' => ['class' => ['webform-help-links']],
+          ],
+        ];
+      }
       $rows[$id] = ['data' => $row, 'no_striping' => TRUE];
     }
 
     $build = [
-      '#theme' => 'table',
-      '#rows' => $rows,
-      '#attributes' => [
-        'border' => 0,
-        'cellpadding' => 2,
-        'cellspacing' => 0,
+      'content' => [
+        '#markup' => '<p>' . $this->t('The below are video tutorials are produced by <a href="https://jrockowitz.com">Jacob Rockowitz</a> and <a href="https://www.webwash.net/">WebWash.net</a>.') . '</p>' .
+          (!$docs ? '<hr/>' : ''),
       ],
     ];
 
     if (!$docs) {
-      $build['#header'] = [
-        ['data' => '', 'style' => 'padding:0; border-top-color: transparent', 'class' => [RESPONSIVE_PRIORITY_LOW]],
-        ['data' => '', 'style' => 'padding:0; border-top-color: transparent'],
+      // Filter.
+      $build['filter'] = [
+        '#type' => 'search',
+        '#title' => $this->t('Filter'),
+        '#title_display' => 'invisible',
+        '#size' => 30,
+        '#placeholder' => $this->t('Filter by videos'),
+        '#attributes' => [
+          'class' => ['webform-form-filter-text'],
+          'data-element' => 'table',
+          'data-source' => 'tbody tr',
+          'data-parent' => 'tr',
+          'data-summary' => '.webform-help-videos-summary',
+          'data-item-singlular' => $this->t('video'),
+          'data-item-plural' => $this->t('videos'),
+          'data-no-results' => '.webform-help-videos-no-results',
+          'title' => $this->t('Enter a keyword to filter by.'),
+          'autofocus' => 'autofocus',
+        ],
       ];
+
+      // Display info.
+      $build['info'] = [
+        '#markup' => $this->t('@total videos', ['@total' => count($rows)]),
+        '#prefix' => '<p class="webform-help-videos-summary">',
+        '#suffix' => '</p>',
+      ];
+
+      // No results.
+      $build['no_results'] = [
+        '#type' => 'webform_message',
+        '#message_message' => $this->t('No videos found. Try a different search.'),
+        '#message_type' => 'info',
+        '#attributes' => ['class' => ['webform-help-videos-no-results']],
+      ];
+
+      $build['table'] = [
+        '#theme' => 'table',
+        '#header' => [
+          ['data' => '', 'style' => 'padding:0; border-top-color: transparent', 'class' => [RESPONSIVE_PRIORITY_LOW]],
+          ['data' => '', 'style' => 'padding:0; border-top-color: transparent'],
+        ],
+        '#rows' => $rows,
+        '#attributes' => [
+          'border' => 0,
+          'cellpadding' => 2,
+          'cellspacing' => 0,
+        ],
+      ];
+
+      $build['#attached']['library'][] = 'webform/webform.admin';
       $build['#attached']['library'][] = 'webform/webform.help';
       $build['#attached']['library'][] = 'webform/webform.ajax';
     }
     else {
-      $build['#no_striping'] = TRUE;
+      $build['videos'] = [
+        '#theme' => 'table',
+        '#rows' => $rows,
+        '#no_striping' => TRUE,
+        '#attributes' => [
+          'border' => 0,
+          'cellpadding' => 2,
+          'cellspacing' => 0,
+        ],
+      ];
     }
 
     return $build;
@@ -440,7 +506,8 @@ public function buildAddOns($docs = FALSE) {
     $build = [
       'content' => [
         '#markup' => '<p>' . $this->t("Below is a list of modules and projects that extend and/or provide additional functionality to the Webform module and Drupal's Form API.") . '</p>' .
-        '<p>★ = ' . $this->t('Recommended') . '</p>',
+          '<hr/>' .
+          '<p>★ = ' . $this->t('Recommended') . '</p>',
       ],
     ];
 
@@ -495,6 +562,10 @@ public function buildLibraries($docs = FALSE) {
     ];
     $libraries = $this->librariesManager->getLibraries();
     foreach ($libraries as $library_name => $library) {
+      if ($docs && !empty($library['deprecated'])) {
+        continue;
+      }
+
       // Get required elements.
       $elements = [];
       if (!empty($library['elements'])) {
@@ -532,10 +603,32 @@ public function buildLibraries($docs = FALSE) {
           '#suffix' => '</dd>',
         ],
       ];
+
       if ($docs) {
         $build['content']['libraries'][$library_name]['title']['#suffix'] = '</dt>';
         unset($build['content']['libraries'][$library_name]['description']['download']);
+
+        if (isset($library['issues_url'])) {
+          $issues_url = $library['issues_url'];
+        }
+        elseif (preg_match('#https://github.com/[^/]+/[^/]+#', $library['download_url']->toString(), $match)) {
+          $issues_url = Url::fromUri($match[0] . '/issues');
+        }
+        else {
+          $issues_url = NULL;
+        }
+
+        if ($issues_url) {
+          $build['content']['libraries'][$library_name]['description']['accessibility'] = [
+            '#type' => 'link',
+            '#title' => $this->t('known accessibility issues'),
+            '#url' => $issues_url->setOption('query', ['q' => 'is:issue is:open accessibility ']),
+            '#prefix' => '<em>@see ',
+            '#suffix' => '</em>',
+          ];
+        }
       }
+
     }
     return $build;
   }
@@ -716,7 +809,10 @@ protected function initGroups() {
    *   An associative array containing videos.
    */
   protected function initVideos() {
-    $videos = [
+    $videos = [];
+
+    // Jacob Rockowitz (jrockowitz.com).
+    $videos += [
       'introduction' => [
         'title' => $this->t('Introduction to Webform for Drupal 8'),
         'content' => $this->t('This screencast provides a general introduction to the Webform module.'),
@@ -877,6 +973,18 @@ protected function initVideos() {
           ],
         ],
       ],
+      'import' => [
+        'title' => $this->t('Importing webform submissions'),
+        'content' => $this->t("This screencast shows how to import submissions using CSV (comma separated values) file."),
+        'youtube_id' => 'AYGr4O-jZBo',
+        'presentation_id' => '189XhD6m0879EMo44ym8uaZaIAFiEl8tkH31WUtge_u8',
+        'links' => [
+          [
+            'title' => $this->t('Webform module now supports importing submissions | Drupal.org'),
+            'url' => 'https://www.drupal.org/node/3040513',
+          ],
+        ],
+      ],
       'configuration' => [
         'title' => $this->t("Configuring the Webform module"),
         'content' => $this->t('This screencast walks through all the configuration settings available to manage forms, submissions, options, handlers, exporters, libraries and assets.'),
@@ -897,10 +1005,31 @@ protected function initVideos() {
           ],
         ],
       ],
+      'access' => [
+        'title' => $this->t("Webform access controls"),
+        'content' => $this->t('This screencast walks through how to use permissions, roles, and custom access rules to control access to webforms and submissions.'),
+        'youtube_id' => 'EPg9Ltwak2M',
+        'presentation_id' => '19Xkb2MR5N075Va403slTVRYjanJ14HmuEYBwwbrQFX4',
+        'links' => [
+          [
+            'title' => $this->t('Users, Roles, and Permissions | Drupal.org'),
+            'url' => 'https://drupal.org/docs/user_guide/en/user-concept.html ',
+          ],
+          [
+            'title' => $this->t('Users, Roles, and Permissions | Drupalize.me'),
+            'url' => 'https://drupalize.me/topic/users-roles-and-permissions',
+          ],
+          [
+            'title' => $this->t('Access Control | Tag1 Consulting'),
+            'url' => 'https://tag1consulting.com/blog/access-control',
+          ],
+        ],
+      ],
       'webform_nodes' => [
         'title' => $this->t('Attaching webforms to nodes'),
         'content' => $this->t('This screencast walks through how to attach a webform to node.'),
         'youtube_id' => 'B_ZyCOVKPqA',
+        'presentation_id' => '1XoIUSgQ0bb_xCfWx8VZe1WHTr0QoCfnE8DzSAsc2WQM',
         'links' => [
           [
             'title' => $this->t('Working with content types and fields | Drupal.org'),
@@ -915,7 +1044,6 @@ protected function initVideos() {
             'url' => 'https://drupalize.me/tutorial/user-guide/planning-data-types',
           ],
         ],
-        'presentation_id' => '1XoIUSgQ0bb_xCfWx8VZe1WHTr0QoCfnE8DzSAsc2WQM',
       ],
       'webform_blocks' => [
         'title' => $this->t('Placing webforms as blocks'),
@@ -969,6 +1097,62 @@ protected function initVideos() {
           ],
         ],
       ],
+      'dialogs' => [
+        'title' => $this->t('Opening webforms in modal dialogs'),
+        'content' => $this->t('This screencast shows how to open webforms in modal dialogs.'),
+        'youtube_id' => 'zmRxyUHWczw',
+        'presentation_id' => '1XlAv-u1lZr13nZvCEuJXtDp4Dmn8X7Fwq_4yac-SajE',
+        'links' => [
+          [
+            'title' => $this->t('Creating a modal in Drupal 8 | Befused'),
+            'url' => 'https://www.drupal.org/project/devel',
+          ],
+          [
+            'title' => $this->t('Display forms in a modal dialog with Drupal 8 | Agaric'),
+            'url' => 'http://agaric.com/blogs/display-forms-modal-dialog-drupal-8',
+          ],
+          [
+            'title' => $this->t('jQueryUI Dialog Documentation'),
+            'url' => 'https://jqueryui.com/dialog/  ',
+          ],
+        ],
+      ],
+      'views' => [
+        'title' => $this->t('Webform views integration'),
+        'content' => $this->t('This presentation shows how to use views to display webform submissions.'),
+        'youtube_id' => 'Qs_m5ybxeXk',
+        'presentation_id' => '1pUUmwjsyxtU9YB4y0qQbSING1W4YBTcqYQjabmSL5N8',
+        'links' => [
+          [
+            'title' => $this->t('Views module | Drupal.org'),
+            'url' => 'https://www.drupal.org/docs/8/core/modules/views',
+          ],
+          [
+            'title' => $this->t('Webform Views Integration | Drupal.org'),
+            'url' => 'https://www.drupal.org/project/webform_views',
+          ],
+          [
+            'title' => $this->t('D8 Webform and Webform Views Integration @ Drupalcamp Colorado'),
+            'url' => 'https://www.youtube.com/watch?v=Riw9g_y1A_s',
+          ],
+        ],
+      ],
+      'attachments' => [
+        'title' => $this->t('Sending webform email attachments'),
+        'content' => $this->t('This presentation shows how to set up and add email attachments via an email handler.'),
+        'youtube_id' => 'w7exQFDIHhQ',
+        'presentation_id' => '1DTE9nSg_CKhWkhBCmfks_o2RoeApTHc4orhNxrj2imk',
+        'links' => [
+          [
+            'title' => $this->t('How to send email attachments? | Drupal.org'),
+            'url' => 'https://www.drupal.org/node/3021480 ',
+          ],
+          [
+            'title' => $this->t('Webform Attachment sub-module | Drupal.org'),
+            'url' => 'https://www.drupal.org/node/3021481',
+          ],
+        ],
+      ],
       'translations' => [
         'title' => $this->t('Translating webforms'),
         'content' => $this->t("This screencast shows how to translate a webform's title, descriptions, label and messages."),
@@ -1011,6 +1195,148 @@ protected function initVideos() {
         'youtube_id' => 'zl_ErUKymYo',
         'presentation_id' => '14vpNvDhYKGhHspu9BurIneTL4C1spyfwsqI82MvTYUA',
       ],
+      'accessibility' => [
+        'title' => $this->t('Webform Accessibility'),
+        'content' => $this->t('This presentation is about approaching accessibility using the Webform module for Drupal 8.'),
+        'youtube_id' => 'JR0wnd6Orfk',
+        'presentation_id' => '1ni2a9id7VT67uO3f0i1UMt9_dswfcSHW1gZcXGCSEcM',
+        'links' => [
+          [
+            'title' => $this->t('Accessibility | Drupal.org'),
+            'url' => 'https://www.drupal.org/about/features/accessibility',
+          ],
+          [
+            'title' => $this->t('Drupal 8 Accessibility'),
+            'url' => 'https://www.drupal.org/docs/8/accessibility',
+          ],
+          [
+            'title' => $this->t('Webform Accessibility'),
+            'url' => 'https://www.drupal.org/docs/8/modules/webform/webform-accessibility',
+          ],
+        ],
+      ],
+      'advanced' => [
+        'title' => $this->t('Advanced Webforms'),
+        'content' => $this->t('This presentation gives you the extra knowledge you need to get the most out the Webform module.'),
+        'youtube_id' => 'Yg2lAzE1heM',
+        'presentation_id' => '1TMo0vBjkdtfcIsYWhxQnjO_rG9ebK64oHhdPvTvwNus',
+      ],
+      'healthcare' => [
+        'title' => $this->t('Webforms for Healthcare'),
+        'content' => $this->t('This presentation discusses how healthcare organizations can leverage the Webform module for Drupal 8.'),
+        'youtube_id' => 'YiK__YobDJw',
+        'presentation_id' => '1jxbJkovaubHrhvjIZ-_OoK0zsANqC1vG4HFvAxfszOE/edit?usp=sharing',
+      ],
+    ];
+
+    // WebWash (www.webwash.net/).
+    $videos += [
+      'webwash_webform' => [
+        'title' => $this->t('How to Create Forms using Webform and Contact in Drupal 8'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to create forms using Webform and Contact module in Drupal 8.'),
+        'youtube_id' => 'u8PBW0K9I9I',
+        'links' => [
+          [
+            'title' => $this->t('Getting Started with Webform in Drupal 8: Part I |  WebWash'),
+            'url' => 'https://www.webwash.net/getting-started-webform-drupal-8/',
+          ],
+          [
+            'title' => $this->t('Moving Forward with Webform in Drupal 8: Part II | WebWash'),
+            'url' => 'https://www.webwash.net/moving-forward-webform-drupal-8/ ',
+          ],
+        ]
+      ],
+      'webwash_install' => [
+        'title' => $this->t('Using Webform in Drupal 8, 1.1: Install Webform'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to download and install the Webform module.'),
+        'youtube_id' => 'T4CiLF8fwFQ',
+      ],
+      'webwash_create' => [
+        'title' => $this->t('Using Webform in Drupal 8, 1.2: Create a Form'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to create a form from scratch and add three elements to it: Name, Email and Telephone.'),
+        'youtube_id' => 'fr3kTiYKNls',
+      ],
+      'webwash_conditional' => [
+        'title' => $this->t('Using Webform in Drupal 8, 2.1: Create Conditional Elements'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to create conditional elements.'),
+        'youtube_id' => 'ic4wu-iZd4Y',
+      ],
+      'webwash_wizard' => [
+        'title' => $this->t('Using Webform in Drupal 8, 2.2: Create Multi-step Wizard'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to create a multi-step page form.'),
+        'youtube_id' => 'k17W2yH71ak',
+      ],
+      'webwash_float' => [
+        'title' => $this->t('Using Webform in Drupal 8, 2.3: Float Elements Next to Each Other'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to float elements next to each other on a form.'),
+        'youtube_id' => 'EgFNqfVboHQ',
+      ],
+      'webwash_options' => [
+        'title' => $this->t('Using Webform in Drupal 8, 2.4: Create List Options'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to create reusable list options for elements.'),
+        'youtube_id' => 'magHXd9DNpg',
+        'links' => [
+          [
+            'title' => $this->t('How to Use Webform Predefined Options in Drupal 8 | WebWash'),
+            'url' => 'https://www.webwash.net/use-webform-predefined-options-drupal-8/',
+          ],
+        ],
+      ],
+      'webwash_email' => [
+        'title' => $this->t('Using Webform in Drupal 8, 2.5: Sending Emails'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to send emails when a submission is submitted.'),
+        'youtube_id' => 'kSzi1J1MyBc',
+      ],
+      'webwash_confirmation' => [
+        'title' => $this->t('Using Webform in Drupal 8, 2.6: Create Confirmation Page'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to create a custom confirmation page.'),
+        'youtube_id' => 'edYCWGoLzZk',
+      ],
+      'webwash_submissions' => [
+        'title' => $this->t('Using Webform in Drupal 8, 3.1: View, Download and Clear Submissions'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to view and manage submission data.'),
+        'youtube_id' => 'dftBF8P4Lh4',
+      ],
+      'webwash_drafts' => [
+        'title' => $this->t('Using Webform in Drupal 8, 3.2: Allow Draft Submissions'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to allow users to save draft submissions.'),
+        'youtube_id' => 'xA3RtJFZc_4',
+      ],
+      'webwash_zapier' => [
+        'title' => $this->t('Using Webform in Drupal 8, 4.1: Send Submissions to Zapier'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to integrate Webform with other system using Zapier.'),
+        'youtube_id' => 'GY0F-rya2iY',
+        'links' => [
+          [
+            'title' => $this->t('Integrate Webform and Google Sheets using Zapier in Drupal 8 | WebWash'),
+            'url' => 'https://www.webwash.net/integrate-webform-and-google-sheets-using-zapier-in-drupal-8/',
+          ],
+        ],
+      ],
+      'webwash_block' => [
+        'title' => $this->t('Using Webform in Drupal 8, 5.1: Display Form as a Block'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to display a form as a block.'),
+        'youtube_id' => 'men4peeDS_4',
+      ],
+      'webwash_node' => [
+        'title' => $this->t('Using Webform in Drupal 8, 5.2: Display Form using Webform Node'),
+        'owner' => $this->t('WebWash'),
+        'content' => $this->t('Learn how to display forms using Webform Node sub-module.'),
+        'youtube_id' => '29pntXdy81k',
+      ],
     ];
     foreach ($videos as $id => &$video_info) {
       $video_info['id'] = $id;
@@ -1176,13 +1502,28 @@ protected function initHelp() {
       ],
     ];
 
+
+    /**************************************************************************/
+    // Help.
+    /**************************************************************************/
+
+    $help['help'] = [
+      'group' => 'help',
+      'title' => $this->t('Help'),
+      'content' => $this->t('Visit the Webform 8.x-5.x <a href="https://www.drupal.org/node/2856146">documentation pages</a> for an <a href="https://www.drupal.org/node/2834423">introduction</a>, <a href="https://www.drupal.org/node/2837024">features overview</a>, <a href="https://www.drupal.org/node/2932764">articles</a>, <a href="https://www.drupal.org/node/2860989">recipes</a>, <a href="https://www.drupal.org/node/2932760">known issues</a>, and a <a href="https://www.drupal.org/node/2843422">roadmap</a>.'),
+      'routes' => [
+        // @see /admin/structure/webform/help
+        'webform.help',
+      ],
+    ];
+
     /**************************************************************************/
     // Contribute.
     /**************************************************************************/
 
     // Contribute.
     $help['contribute'] = [
-      'group' => 'contribute',
+      'group' => 'help',
       'title' => $this->t('Contribute'),
       'content' => $this->t('The <strong>Contribute</strong> page encourages individuals and organizations to join the Drupal community, become members of the Drupal Association, and contribute to Drupal projects, events, and more.'),
       'video_id' => 'about',
@@ -1191,6 +1532,7 @@ protected function initHelp() {
         'webform.contribute',
       ],
     ];
+
     /**************************************************************************/
     // Configuration.
     /**************************************************************************/
@@ -1282,20 +1624,37 @@ protected function initHelp() {
     ];
 
     // Configuration: Libraries.
+    $t_args = [
+      '@webform-libraries-make' => 'webform-libraries-make',
+      '@webform-libraries-composer' => 'webform-libraries-composer',
+      '@webform-libraries-download' => 'webform-libraries-download',
+      '@webform-composer-update' => 'webform-composer-update',
+    ];
+    $drush_version = (class_exists('\Drush\Drush')) ? \Drush\Drush::getMajorVersion() : 8;
+    if ($drush_version >= 9) {
+      foreach ($t_args as $command_name => $command) {
+        $t_args[$command_name] = str_replace('-', ':', $command);
+      }
+    }
     $help['config_libraries_help'] = [
       'group' => 'configuration',
       'title' => $this->t('Configuration: Libraries: Help'),
       'content' => '<p>' . $this->t('The Webform module utilizes third-party Open Source libraries to enhance webform elements and to provide additional functionality.') . ' ' .
-        $this->t('It is recommended that these libraries are installed in your Drupal installations /libraries directory.') . ' ' .
+        $this->t('It is recommended that these libraries are installed in your Drupal installations /libraries or /web/libraries directory.') . ' ' .
         $this->t('If these libraries are not installed, they will be automatically loaded from a CDN.') . ' ' .
         $this->t('All libraries are optional and can be excluded via the admin settings form.') .
         '</p>' .
         '<p>' . $this->t('There are several ways to download the needed third-party libraries.') . '</p>' .
+        '<p><strong>' . $this->t('Recommended') . '</strong></p>' .
         '<ul>' .
-        '<li>' . $this->t('Generate a *.make.yml or composer.json file using <code>drush webform-libraries-make</code> or <code>drush webform-libraries-composer</code>.') . '</li>' .
-        '<li>' . $this->t('Execute <code>drush webform-libraries-download</code>, which will download third-party libraries required by the Webform module.') . '</li>' .
-        '<li>' . $this->t("Execute <code>drush webform-composer-update</code>, which will update your Drupal installation's composer.json to include the Webform module's selected libraries as repositories.") . '</li>' .
-        '<li>' . $this->t('Download and extract a <a href=":href">zipped archive containing all webform libraries</a> and extract the directories and files to /libraries', [':href' => 'https://cgit.drupalcode.org/sandbox-jrockowitz-2941983/plain/libraries.zip']) . '</li>' .
+        '<li>' . $this->t('Use the <a href="https://github.com/wikimedia/composer-merge-plugin">Composer Merge plugin</a> to include the Webform module\'s <a href="https://cgit.drupalcode.org/webform/tree/composer.libraries.json">composer.libraries.json</a> or generate a custom file using <code>drush @webform-libraries-composer &gt; DRUPAL_ROOT/composer.libraries.json</code>.', $t_args) . '<br/><strong>' . t('<a href="https://www.drupal.org/node/3003140">Learn more &raquo;</a>') . '</strong>'. '</li>' .
+        '</ul>' .
+        '<p><strong>' . $this->t('Alternatives') . '</strong></p>' .
+        '<ul>' .
+        '<li>' . $this->t('Generate a *.make.yml or composer.json file using <code>drush @webform-libraries-make</code> or <code>drush @webform-libraries-composer</code>.', $t_args) . '</li>' .
+        '<li>' . $this->t('Execute <code>drush @webform-libraries-download</code>, to download third-party libraries required by the Webform module. (OSX/Linux)', $t_args) . '</li>' .
+        '<li>' . $this->t("Execute <code>drush @webform-composer-update</code>, to update your Drupal installation's composer.json to include the Webform module's selected libraries as repositories.", $t_args) . '</li>' .
+        '<li>' . $this->t('Download and extract a <a href=":href">zipped archive containing all webform libraries</a> and extract the directories and files to /libraries or /web/libraries', [':href' => 'https://cgit.drupalcode.org/sandbox-jrockowitz-2941983/plain/libraries.zip']) . '</li>' .
         '</ul>',
       'message_type' => 'info',
       'message_close' => TRUE,
@@ -1342,8 +1701,8 @@ protected function initHelp() {
         $this->t('<strong>Webform Element</strong> plugins are used to enhance existing render/form elements. Webform element plugins provide default properties, data normalization, custom validation, element configuration form and customizable display formats.'),
       'video_id' => 'plugins',
       'routes' => [
-        // @see /admin/structure/webform/plugins/elements
-        'webform.element_plugins',
+        // @see /admin/reports/webform-plugins/elements
+        'webform.reports_plugins.elements',
       ],
     ];
 
@@ -1355,8 +1714,8 @@ protected function initHelp() {
         $this->t('<strong>Handlers</strong> are used to route submitted data to external applications and send notifications & confirmations.'),
       'video_id' => 'plugins',
       'routes' => [
-        // @see /admin/structure/webform/plugins/handlers
-        'webform.handler_plugins',
+        // @see /admin/reports/webform-plugins/handlers
+        'webform.reports_plugins.handlers',
       ],
     ];
 
@@ -1368,8 +1727,8 @@ protected function initHelp() {
         $this->t('<strong>Exporters</strong> are used to export results into a downloadable format that can be used by MS Excel, Google Sheets and other spreadsheet applications.'),
       'video_id' => 'plugins',
       'routes' => [
-        // @see /admin/structure/webform/plugins/exporters
-        'webform.exporter_plugins',
+        // @see /admin/reports/webform-plugins/exporters
+        'webform.reports_plugins.exporters',
       ],
     ];
 
@@ -1395,7 +1754,7 @@ protected function initHelp() {
       'group' => 'forms',
       'title' => $this->t('Webform: Test'),
       'content' => $this->t("The <strong>Test</strong> form allows a webform to be tested using a customizable test dataset.") . ' ' .
-        $this->t('Multiple test submissions can be created using the devel_generate module.'),
+        $this->t('Multiple test submissions can be created using the Devel generate module.'),
       'video_id' => 'forms',
       'routes' => [
         // @see /admin/structure/webform/manage/{webform}/test
@@ -1554,30 +1913,6 @@ protected function initHelp() {
       ],
     ];
 
-    // Submissions: Purge.
-    $help['submissions_purge'] = [
-      'group' => 'submissions',
-      'title' => $this->t('Submissions: Purge'),
-      'content' => $this->t('The <strong>Submissions purge</strong> page allows all submissions across all webforms to be deleted. <strong>PLEASE NOTE: THIS ACTION CANNOT BE UNDONE.</strong>'),
-      'message_type' => 'warning',
-      'routes' => [
-        // @see /admin/structure/webform/results/purge
-        'entity.webform_submission.collection_purge',
-      ],
-    ];
-
-    // Submissions: Log.
-    $help['submissions_log'] = [
-      'group' => 'submissions',
-      'title' => $this->t('Submissions: Log'),
-      'content' => $this->t('The <strong>Submissions log</strong> page tracks all submission events for all webforms that have submission logging enabled. Submission logging can be enabled globally or on a per webform basis.'),
-      'routes' => [
-        // @see /admin/structure/webform/results/log
-        'entity.webform_submission.collection_log',
-      ],
-    ];
-
-
     // Results.
     $help['results'] = [
       'group' => 'submissions',
@@ -1591,17 +1926,6 @@ protected function initHelp() {
       ],
     ];
 
-    // Results: Log.
-    $help['results_log'] = [
-      'group' => 'submissions',
-      'title' => $this->t('Results: Log'),
-      'content' => $this->t('The <strong>Results Log</strong> lists all webform submission events for the current webform.'),
-      'routes' => [
-        // @see /admin/structure/webform/manage/{webform}/results/log
-        'entity.webform.results_log',
-      ],
-    ];
-
     // Results: Download.
     $help['results_download'] = [
       'group' => 'submissions',
@@ -1613,17 +1937,6 @@ protected function initHelp() {
       ],
     ];
 
-    // Results: Clear.
-    $help['results_clear'] = [
-      'group' => 'submissions',
-      'title' => $this->t('Results: Clear'),
-      'content' => $this->t("The <strong>Clear</strong> page allows all submissions to a webform to be deleted."),
-      'routes' => [
-        // @see /admin/structure/webform/manage/{webform}/results/clear
-        'entity.webform.results_clear',
-      ],
-    ];
-
     /**************************************************************************/
     // Submission.
     /**************************************************************************/
@@ -1680,19 +1993,6 @@ protected function initHelp() {
       ],
     ];
 
-    $help['submission_log'] = [
-      'group' => 'submission',
-      'title' => $this->t('Submission: Log'),
-      'content' => $this->t("The <strong>Log</strong> page shows all events and transactions for a submission."),
-      'video_id' => 'submission',
-      'routes' => [
-        // @see /admin/structure/webform/manage/{webform}/submission/{webform_submisssion}/log
-        'entity.webform_submission.log',
-        // @see /node/{node}/webform/submission/{webform_submission}/log
-        'entity.node.webform_submission.log',
-      ],
-    ];
-
     $help['submission_edit'] = [
       'group' => 'submission',
       'title' => $this->t('Submission: Edit'),
@@ -1808,15 +2108,6 @@ protected function initHelp() {
         'entity.node.webform.results_submissions',
       ],
     ];
-    $help['webform_node_results_log'] = [
-      'group' => 'webform_nodes',
-      'title' => $this->t('Webform Node: Results: Log'),
-      'content' => $this->t('The <strong>Results Log</strong> lists all webform submission events for the current webform.'),
-      'routes' => [
-        // @see /node/{node}/webform/results/log
-        'entity.node.webform.results_log',
-      ],
-    ];
     $help['webform_node_results_download'] = [
       'group' => 'webform_nodes',
       'title' => $this->t('Webform Node: Results: Download'),
@@ -1826,15 +2117,6 @@ protected function initHelp() {
         'entity.node.webform.results_export',
       ],
     ];
-    $help['webform_node_results_clear'] = [
-      'group' => 'webform_nodes',
-      'title' => $this->t('Webform Node: Results: Clear'),
-      'content' => $this->t("The <strong>Clear</strong> page allows all submissions to a webform node to be deleted."),
-      'routes' => [
-        // @see /node/{node}/webform/results/clear
-        'entity.node.webform.results_clear',
-      ],
-    ];
 
     // Webform Block.
     $help['webform_block'] = [
@@ -1847,6 +2129,22 @@ protected function initHelp() {
       ],
     ];
 
+    // Webform Accessibility.
+    $help['webform_accessibility'] = [
+      'group' => 'webform_accessibility',
+      'title' => $this->t('Webform Node'),
+      'content' => $this->t("The Webform module aims to be accessible to all users."),
+      'video_id' => 'accessibility',
+      'paths' => [
+        '/admin/structure/webform/manage/example_accessibility_*',
+      ],
+      'message_type' => 'info',
+      'message_close' => TRUE,
+      'message_storage' => WebformMessage::STORAGE_USER,
+      'access' => $this->currentUser->hasPermission('administer webform'),
+      'weight' => -10,
+    ];
+
     /**************************************************************************/
     // Messages.
     /**************************************************************************/
diff --git a/web/modules/webform/src/WebformHelpManagerInterface.php b/web/modules/webform/src/WebformHelpManagerInterface.php
index dc25477983eecdf2d3bbffc5afce44d121f02d02..3d3e416cd6931b4f278c483008fee2ec628651e7 100644
--- a/web/modules/webform/src/WebformHelpManagerInterface.php
+++ b/web/modules/webform/src/WebformHelpManagerInterface.php
@@ -43,7 +43,7 @@ public function getHelp($id = NULL);
   public function getVideo($id = NULL);
 
   /**
-   * Get video. links
+   * Get video links.
    *
    * @param string $id
    *   Video id.
@@ -57,7 +57,7 @@ public function getVideoLinks($id);
    * Sets a notification to be displayed to webform administrators.
    *
    * @param string $id
-   *   The notification id
+   *   The notification id.
    * @param string|\Drupal\Component\Render\MarkupInterface|array $message
    *   The notification to be displayed to webform administrators.
    * @param string $type
@@ -97,7 +97,7 @@ public function getNotifications($type = NULL);
    * Delete a notification by id.
    *
    * @param string $id
-   *   The notification id
+   *   The notification id.
    *
    * @internal
    *   Currently being used to display notifications related to updates.
diff --git a/web/modules/webform/src/WebformInterface.php b/web/modules/webform/src/WebformInterface.php
index 755e0e9f2d99ff40fe7930647e83be26d9c5c64f..b32eb2d667939489de8c40003421d5c4041d2ec0 100644
--- a/web/modules/webform/src/WebformInterface.php
+++ b/web/modules/webform/src/WebformInterface.php
@@ -4,7 +4,6 @@
 
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
 use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
-use Drupal\Core\Session\AccountInterface;
 use Drupal\user\EntityOwnerInterface;
 use Drupal\webform\Plugin\WebformHandlerInterface;
 
@@ -13,6 +12,26 @@
  */
 interface WebformInterface extends ConfigEntityInterface, EntityWithPluginCollectionInterface, EntityOwnerInterface {
 
+  /**
+   * Webform title.
+   */
+  const TITLE_WEBFORM = 'webform';
+
+  /**
+   * Source entity title.
+   */
+  const TITLE_SOURCE_ENTITY = 'source_entity';
+
+  /**
+   * Both source entity and webform title.
+   */
+  const TITLE_SOURCE_ENTITY_WEBFORM = 'source_entity_webform';
+
+  /**
+   * Both webform and source entity title.
+   */
+  const TITLE_WEBFORM_SOURCE_ENTITY = 'webform_source_entity';
+
   /**
    * Denote drafts are not allowed.
    *
@@ -49,6 +68,11 @@ interface WebformInterface extends ConfigEntityInterface, EntityWithPluginCollec
    */
   const STATUS_SCHEDULED = 'scheduled';
 
+  /**
+   * Webform status archived.
+   */
+  const STATUS_ARCHIVED = 'archived';
+
   /**
    * Webform confirmation page.
    */
@@ -84,6 +108,31 @@ interface WebformInterface extends ConfigEntityInterface, EntityWithPluginCollec
    */
   const CONFIRMATION_DEFAULT = 'default';
 
+  /**
+   * Webform confirmation none.
+   */
+  const CONFIRMATION_NONE = 'none';
+
+  /**
+   * Display standard 403 access denied page.
+   */
+  const ACCESS_DENIED_DEFAULT = 'default';
+
+  /**
+   * Display customized access denied message.
+   */
+  const ACCESS_DENIED_MESSAGE = 'message';
+
+  /**
+   * Display customized 403 access denied page.
+   */
+  const ACCESS_DENIED_PAGE = 'page';
+
+  /**
+   * Redirect to user login with custom message.
+   */
+  const ACCESS_DENIED_LOGIN = 'login';
+
   /**
    * Returns the webform's (original) langcode.
    *
@@ -92,6 +141,16 @@ interface WebformInterface extends ConfigEntityInterface, EntityWithPluginCollec
    */
   public function getLangcode();
 
+  /**
+   * Returns the webform's weight.
+   *
+   * Only applies to when multiple webforms are attached to a single node.
+   *
+   * @return int
+   *   The webform's weight.
+   */
+  public function getWeight();
+
   /**
    * Determine if the webform has page or is attached to other entities.
    *
@@ -108,6 +167,14 @@ public function hasPage();
    */
   public function hasManagedFile();
 
+  /**
+   * Determine if the webform's elements include attachments.
+   *
+   * @return bool
+   *   TRUE if the webform's elements include attachments.
+   */
+  public function hasAttachments();
+
   /**
    * Determine if the webform is using a Flexbox layout.
    *
@@ -165,10 +232,10 @@ public function getNumberOfActions();
   public function hasPreview();
 
   /**
-   * Determine if the webform has multistep form wizard pages.
+   * Determine if the webform has multi-step form wizard pages.
    *
    * @return bool
-   *   TRUE if the webform has multistep form wizard pages.
+   *   TRUE if the webform has multi-step form wizard pages.
    */
   public function hasWizardPages();
 
@@ -180,6 +247,34 @@ public function hasWizardPages();
    */
   public function getNumberOfWizardPages();
 
+  /**
+   * Returns the webform's current operation.
+   *
+   * @return string
+   *   The webform's operation.
+   */
+  public function getOperation();
+
+  /**
+   * Sets the webform's current operation .
+   *
+   * @param string $operation
+   *   The webform's operation.
+   *
+   * @return $this
+   *
+   * @see \Drupal\webform\WebformSubmissionForm
+   */
+  public function setOperation($operation);
+
+  /**
+   * Determine if the webform is being tested.
+   *
+   * @return bool
+   *   TRUE if the webform is being tested.
+   */
+  public function isTest();
+
   /**
    * Sets the webform settings and properties override state.
    *
@@ -258,14 +353,30 @@ public function isOpening();
    */
   public function isTemplate();
 
+  /**
+   * Returns the webform archive indicator.
+   *
+   * @return bool
+   *   TRUE if the webform is archived.
+   */
+  public function isArchived();
+
   /**
    * Returns the webform confidential indicator.
    *
    * @return bool
-   *   TRUE if the webform is confidential .
+   *   TRUE if the webform is confidential.
    */
   public function isConfidential();
 
+  /**
+   * Determine if remote IP address is being stored.
+   *
+   * @return bool
+   *   TRUE if remote IP address is being stored.
+   */
+  public function hasRemoteAddr();
+
   /**
    * Determine if the saving of submissions is disabled.
    *
@@ -453,18 +564,18 @@ public function setSettingOverride($key, $value);
   public function setPropertyOverride($property_name, $value);
 
   /**
-   * Returns the webform access controls.
+   * Returns the webform access rules.
    *
    * @return array
-   *   A structured array containing all the webform access controls.
+   *   A structured array containing all the webform access rules.
    */
   public function getAccessRules();
 
   /**
-   * Sets the webform access.
+   * Sets the webform access rules.
    *
    * @param array $access
-   *   The structured array containing all the webform access controls.
+   *   The structured array containing all the webform access rules.
    *
    * @return $this
    */
@@ -478,30 +589,6 @@ public function setAccessRules(array $access);
    */
   public static function getDefaultSettings();
 
-  /**
-   * Returns the webform default access controls.
-   *
-   * @return array
-   *   A structured array containing all the webform default access controls.
-   */
-  public static function getDefaultAccessRules();
-
-  /**
-   * Checks webform access to an operation on a webform's submission.
-   *
-   * @param string $operation
-   *   The operation access should be checked for.
-   *   Usually "create", "view", "update", "delete", "purge", or "admin".
-   * @param \Drupal\Core\Session\AccountInterface $account
-   *   The user session for which to check access.
-   * @param \Drupal\webform\WebformSubmissionInterface|null $webform_submission
-   *   (optional) A webform submission.
-   *
-   * @return \Drupal\Core\Access\AccessResultInterface
-   *   The access result.
-   */
-  public function checkAccessRules($operation, AccountInterface $account, WebformSubmissionInterface $webform_submission = NULL);
-
   /**
    * Get webform submission webform.
    *
@@ -616,13 +703,37 @@ public function getElementsInitializedAndFlattened($operation = NULL);
   public function getElementsInitializedFlattenedAndHasValue($operation = NULL);
 
   /**
-   * Get webform elements selectors as options.
+   * Get webform managed file elements.
+   *
+   * @return array
+   *   Webform managed file elements.
+   */
+  public function getElementsManagedFiles();
+
+  /**
+   * Get webform attachemnt elements.
+   *
+   * @return array
+   *   Webform attachment elements.
+   */
+  public function getElementsAttachments();
+
+  /**
+   * Get webform element's selectors as options.
    *
    * @return array
    *   Webform elements selectors as options.
    */
   public function getElementsSelectorOptions();
 
+  /**
+   * Get webform element options as autocomplete source values.
+   *
+   * @return array
+   *   Webform element options as autocomplete source values.
+   */
+  public function getElementsSelectorSourceValues();
+
   /**
    * Get webform elements that can be prepopulated.
    *
@@ -777,7 +888,7 @@ public function deleteWebformHandler(WebformHandlerInterface $handler);
    * Invoke a handlers method.
    *
    * @param string $method
-   *   The handle method to be invoked.
+   *   The handler method to be invoked.
    * @param mixed $data
    *   The argument to passed by reference to the handler method.
    * @param mixed $context1
@@ -791,7 +902,7 @@ public function invokeHandlers($method, &$data, &$context1 = NULL, &$context2 =
    * Invoke elements method.
    *
    * @param string $method
-   *   The handle method to be invoked.
+   *   The handler method to be invoked.
    * @param mixed $data
    *   The argument to passed by reference to the handler method.
    * @param mixed $context1
diff --git a/web/modules/webform/src/WebformLibrariesManager.php b/web/modules/webform/src/WebformLibrariesManager.php
index b808744f11ea138a359092bf1541454aa272821e..5f285ba91e072947190dcbc1734c4386661ce0fd 100644
--- a/web/modules/webform/src/WebformLibrariesManager.php
+++ b/web/modules/webform/src/WebformLibrariesManager.php
@@ -102,6 +102,12 @@ public function requirements($cli = FALSE) {
     ];
 
     foreach ($libraries as $library_name => $library) {
+      // Excluded.
+      if ($this->isExcluded($library_name)) {
+        $stats['@excluded']++;
+        continue;
+      }
+
       $library_path = '/libraries/' . $library_name;
       $library_exists = (file_exists(DRUPAL_ROOT . $library_path)) ? TRUE : FALSE;
 
@@ -117,17 +123,13 @@ public function requirements($cli = FALSE) {
         ':settings_elements_href' => Url::fromRoute('webform.config.elements')->toString(),
       ];
 
-      if ($this->isExcluded($library_name)) {
-        // Excluded.
-        $stats['@excluded']++;
-        $title = $this->t('<strong>@title</strong> (Excluded)', $t_args);
-        if (!empty($library['elements']) && $this->areElementsExcluded($library['elements'])) {
-          $t_args['@element_type'] = implode('; ', $library['elements']);
-          $description = $this->t('The <a href=":homepage_href">@title</a> library is excluded because required element types (@element_type) are <a href=":settings_elements_href">excluded</a>.', $t_args);
-        }
-        else {
-          $description = $this->t('The <a href=":homepage_href">@title</a> library is <a href=":settings_libraries_href">excluded</a>.', $t_args);
-        }
+      if (!empty($library['module'])) {
+        // Installed by module.
+        $t_args['@module'] = $library['module'];
+        $t_args[':module_href'] = 'https://www.drupal.org/project/' . $library['module'];
+        $stats['@installed']++;
+        $title = $this->t('<strong>@title</strong> (Installed)', $t_args);
+        $description = $this->t('The <a href=":homepage_href">@title</a> library is installed by the <b><a href=":module_href">@module</a></b> module.', $t_args);
       }
       elseif ($library_exists) {
         // Installed.
@@ -192,7 +194,7 @@ public function requirements($cli = FALSE) {
    */
   public function getLibrary($name) {
     $libraries = $this->getLibraries();
-    return $libraries [$name];
+    return $libraries[$name];
   }
 
   /**
@@ -281,10 +283,9 @@ protected function initLibraries() {
       'plugin_path' => 'libraries/ckeditor.autogrow/',
       'plugin_url' => "https://cdn.rawgit.com/ckeditor/ckeditor-dev/$ckeditor_version/plugins/autogrow/",
       'version' => $ckeditor_version,
-      'optional' => TRUE,
     ];
     $libraries['ckeditor.fakeobjects'] = [
-      'title' => $this->t('CKEditor: Fakeobjects'),
+      'title' => $this->t('CKEditor: Fake Objects'),
       'description' => $this->t('Utility required by CKEditor link plugin.'),
       'notes' => $this->t('Allows CKEditor to use basic image and link dialog.'),
       'homepage_url' => Url::fromUri('https://ckeditor.com/addon/fakeobjects'),
@@ -292,7 +293,6 @@ protected function initLibraries() {
       'plugin_path' => 'libraries/ckeditor.fakeobjects/',
       'plugin_url' => "https://cdn.rawgit.com/ckeditor/ckeditor-dev/$ckeditor_version/plugins/fakeobjects/",
       'version' => $ckeditor_version,
-      'optional' => TRUE,
     ];
     $libraries['ckeditor.image'] = [
       'title' => $this->t('CKEditor: Image'),
@@ -303,7 +303,6 @@ protected function initLibraries() {
       'plugin_path' => 'libraries/ckeditor.image/',
       'plugin_url' => "https://cdn.rawgit.com/ckeditor/ckeditor-dev/$ckeditor_version/plugins/image/",
       'version' => $ckeditor_version,
-      'optional' => TRUE,
     ];
     $libraries['ckeditor.link'] = [
       'title' => $this->t('CKEditor: Link'),
@@ -314,27 +313,38 @@ protected function initLibraries() {
       'plugin_path' => 'libraries/ckeditor.link/',
       'plugin_url' => "https://cdn.rawgit.com/ckeditor/ckeditor-dev/$ckeditor_version/plugins/link/",
       'version' => $ckeditor_version,
-      'optional' => TRUE,
     ];
     $libraries['ckeditor.codemirror'] = [
       'title' => $this->t('CKEditor: CodeMirror'),
       'description' => $this->t('Provides syntax highlighting for the CKEditor with the CodeMirror Plugin.'),
       'notes' => $this->t('Makes it easier to edit the HTML source.'),
       'homepage_url' => Url::fromUri('https://github.com/w8tcha/CKEditor-CodeMirror-Plugin'),
-      'download_url' => Url::fromUri('https://github.com/w8tcha/CKEditor-CodeMirror-Plugin/releases/download/v1.17.5/CKEditor-CodeMirror-Plugin.zip'),
+      'download_url' => Url::fromUri('https://github.com/w8tcha/CKEditor-CodeMirror-Plugin/releases/download/v1.17.7/CKEditor-CodeMirror-Plugin.zip'),
       'plugin_path' => 'libraries/ckeditor.codemirror/codemirror/',
-      'plugin_url' => "https://cdn.rawgit.com/w8tcha/CKEditor-CodeMirror-Plugin/v1.17.5/codemirror/",
-      'version' => 'v1.17.5',
-      'optional' => TRUE,
+      'plugin_url' => "https://cdn.rawgit.com/w8tcha/CKEditor-CodeMirror-Plugin/v1.17.7/codemirror/",
+      'version' => 'v1.17.7',
     ];
     $libraries['codemirror'] = [
       'title' => $this->t('Code Mirror'),
       'description' => $this->t('Code Mirror is a versatile text editor implemented in JavaScript for the browser.'),
       'notes' => $this->t('Code Mirror is used to provide a text editor for YAML, HTML, CSS, and JavaScript configuration settings and messages.'),
       'homepage_url' => Url::fromUri('http://codemirror.net/'),
-      'download_url' => Url::fromUri('https://github.com/components/codemirror/archive/5.36.0.zip'),
-      'version' => '5.36.0',
-      'optional' => TRUE,
+      'download_url' => Url::fromUri('https://github.com/components/codemirror/archive/5.44.0.zip'),
+      'issues_url' => Url::fromUri('https://github.com/codemirror/codemirror/issues'),
+      'version' => '5.44.0',
+    ];
+    $libraries['algolia.places'] = [
+      'title' => $this->t('Algolia Places'),
+      'description' => $this->t('Algolia Places provides a fast, distributed and easy way to use an address search autocomplete JavaScript library on your website.'),
+      'notes' => $this->t('Algolia Places is by the location places elements.'),
+      'homepage_url' => Url::fromUri('https://github.com/algolia/places'),
+      'issues_url' => Url::fromUri('https://github.com/algolia/places/issues'),
+      // NOTE: Using NPM/JsDelivr because it contains the '/dist/cdn/' directory.
+      // @see https://asset-packagist.org/package/detail?fullname=npm-asset/places.js
+      // @see https://www.jsdelivr.com/package/npm/places.js
+      'download_url' => Url::fromUri('https://registry.npmjs.org/places.js/-/places.js-1.16.1.tgz'),
+      'version' => '1.16.1',
+      'elements' => ['webform_location_places'],
     ];
     $libraries['jquery.geocomplete'] = [
       'title' => $this->t('jQuery: Geocoding and Places Autocomplete Plugin'),
@@ -343,7 +353,8 @@ protected function initLibraries() {
       'homepage_url' => Url::fromUri('http://ubilabs.github.io/geocomplete/'),
       'download_url' => Url::fromUri('https://github.com/ubilabs/geocomplete/archive/1.7.0.zip'),
       'version' => '1.7.0',
-      'elements' => ['webform_location'],
+      'elements' => ['webform_location_geocomplete'],
+      'deprecated' => $this->t('The jQuery: Geocoding and Places Autocomplete Plugin library is not being maintained. It has been <a href=":href">deprecated</a> and will be removed before Webform 8.x-6.0.', [':href' => 'https://www.drupal.org/node/2991275']),
     ];
     $libraries['jquery.icheck'] = [
       'title' => $this->t('jQuery: iCheck'),
@@ -352,31 +363,28 @@ protected function initLibraries() {
       'homepage_url' => Url::fromUri('http://icheck.fronteed.com/'),
       'download_url' => Url::fromUri('https://github.com/fronteed/icheck/archive/1.0.2.zip'),
       'version' => '1.0.2 ',
-      'optional' => TRUE,
-      'deprecated' => $this->t('The iCheck library is not being maintained and has been <a href=":href">deprecated</a>. It wil be removed before Webform 8.x-5.0.', [':href' => 'https://www.drupal.org/project/webform/issues/2931154']),
+      'deprecated' => $this->t('The iCheck library is not being maintained. It has been <a href=":href">deprecated</a> and will be removed before Webform 8.x-6.0.', [':href' => 'https://www.drupal.org/project/webform/issues/2931154']),
     ];
     $libraries['jquery.inputmask'] = [
       'title' => $this->t('jQuery: Input Mask'),
-      'description' => $this->t('Input masks ensures a predefined format is entered. This can be useful for dates, numerics, phone numbers, etc...'),
+      'description' => $this->t('Input masks ensures a predefined format is entered. This can be useful for dates, numerics, phone numbers, etc…'),
       'notes' => $this->t('Input masks are used to ensure predefined and custom formats for text fields.'),
       'homepage_url' => Url::fromUri('https://robinherbots.github.io/Inputmask/'),
-      'download_url' => Url::fromUri('https://github.com/RobinHerbots/jquery.inputmask/archive/3.3.11.zip'),
-      'version' => '3.3.11',
-      'optional' => TRUE,
+      'download_url' => Url::fromUri('https://github.com/RobinHerbots/jquery.inputmask/archive/4.0.6.zip'),
+      'version' => '4.0.6',
     ];
     $libraries['jquery.intl-tel-input'] = [
       'title' => $this->t('jQuery: International Telephone Input'),
       'description' => $this->t("A jQuery plugin for entering and validating international telephone numbers. It adds a flag dropdown to any input, detects the user's country, displays a relevant placeholder and provides formatting/validation methods."),
       'notes' => $this->t('International Telephone Input is used by the Telephone element.'),
       'homepage_url' => Url::fromUri('https://github.com/jackocnr/intl-tel-input'),
-      'download_url' => Url::fromUri('https://github.com/jackocnr/intl-tel-input/archive/v12.1.12.zip'),
-      'version' => '12.1.12',
-      'optional' => TRUE,
+      'download_url' => Url::fromUri('https://github.com/jackocnr/intl-tel-input/archive/v15.0.1.zip'),
+      'version' => '15.0.1',
     ];
     $libraries['jquery.rateit'] = [
       'title' => $this->t('jQuery: RateIt'),
       'description' => $this->t("Rating plugin for jQuery. Fast, progressive enhancement, touch support, customizable (just swap out the images, or change some CSS), unobtrusive JavaScript (using HTML5 data-* attributes), RTL support. The Rating plugin supports as many stars as you'd like, and also any step size."),
-      'notes' => $this->t('RateIt is used to provide a customizable rating webform element.'),
+      'notes' => $this->t('RateIt is used to provide a customizable rating element.'),
       'homepage_url' => Url::fromUri('https://github.com/gjunge/rateit.js'),
       'download_url' => Url::fromUri('https://github.com/gjunge/rateit.js/archive/1.1.1.zip'),
       'version' => '1.1.1',
@@ -389,25 +397,32 @@ protected function initLibraries() {
       'homepage_url' => Url::fromUri('https://select2.github.io/'),
       'download_url' => Url::fromUri('https://github.com/select2/select2/archive/4.0.5.zip'),
       'version' => '4.0.5',
-      'optional' => TRUE,
+      'module' => $this->moduleHandler->moduleExists('select2') ? 'select2' : '',
     ];
     $libraries['jquery.chosen'] = [
       'title' => $this->t('jQuery: Chosen'),
       'description' => $this->t('A jQuery plugin that makes long, unwieldy select boxes much more user-friendly.'),
       'notes' => $this->t('Chosen is used to improve the user experience for select menus. Chosen is an alternative to Select2.'),
       'homepage_url' => Url::fromUri('https://harvesthq.github.io/chosen/'),
-      'download_url' => Url::fromUri('https://github.com/harvesthq/chosen/releases/download/v1.8.3/chosen_v1.8.3.zip'),
-      'version' => '1.8.3',
-      'optional' => TRUE,
+      'download_url' => Url::fromUri('https://github.com/harvesthq/chosen/releases/download/v1.8.7/chosen_v1.8.7.zip'),
+      'version' => '1.8.7',
+      'module' => $this->moduleHandler->moduleExists('chosen') ? 'chosen' : '',
+    ];
+    $libraries['jquery.textcounter'] = [
+      'title' => $this->t('jQuery: Text Counter'),
+      'description' => $this->t('A jQuery plugin for counting and limiting characters/words on text input, or textarea, elements.'),
+      'notes' => $this->t('Word or character counting, with server-side validation, is available for text fields and text areas.'),
+      'homepage_url' => Url::fromUri('https://github.com/ractoon/jQuery-Text-Counter'),
+      'download_url' => Url::fromUri('https://github.com/ractoon/jQuery-Text-Counter/archive/0.8.0.zip'),
+      'version' => '0.8.0',
     ];
     $libraries['jquery.timepicker'] = [
       'title' => $this->t('jQuery: Timepicker'),
       'description' => $this->t('A lightweight, customizable javascript timepicker plugin for jQuery, inspired by Google Calendar.'),
       'notes' => $this->t('Timepicker is used to provide a polyfill for HTML 5 time elements.'),
       'homepage_url' => Url::fromUri('https://github.com/jonthornton/jquery-timepicker'),
-      'download_url' => Url::fromUri('https://github.com/jonthornton/jquery-timepicker/archive/1.11.13.zip'),
-      'version' => '1.11.13',
-      'optional' => TRUE,
+      'download_url' => Url::fromUri('https://github.com/jonthornton/jquery-timepicker/archive/1.11.14.zip'),
+      'version' => '1.11.14',
     ];
     $libraries['jquery.toggles'] = [
       'title' => $this->t('jQuery: Toggles'),
@@ -417,24 +432,15 @@ protected function initLibraries() {
       'download_url' => Url::fromUri('https://github.com/simontabor/jquery-toggles/archive/v4.0.0.zip'),
       'version' => '4.0.0',
       'elements' => ['webform_toggle', 'webform_toggles'],
-    ];
-    $libraries['jquery.word-and-character-counter'] = [
-      'title' => $this->t('jQuery: Word and character counter plug-in!'),
-      'description' => $this->t('The jQuery word and character counter plug-in allows you to count characters or words'),
-      'notes' => $this->t('Word or character counting, with server-side validation, is available for text fields and text areas.'),
-      'homepage_url' => Url::fromUri('https://github.com/qwertypants/jQuery-Word-and-Character-Counter-Plugin'),
-      'download_url' => Url::fromUri('https://github.com/qwertypants/jQuery-Word-and-Character-Counter-Plugin/archive/2.5.1.zip'),
-      'version' => '2.5.1',
-      'optional' => TRUE,
+      'deprecated' => $this->t('The Toogles library is not being maintained and has major accessibility issues. It has been <a href=":href">deprecated</a> and will be removed before Webform 8.x-6.0.', [':href' => 'https://www.drupal.org/project/webform/issues/2890861']),
     ];
     $libraries['progress-tracker'] = [
       'title' => $this->t('Progress Tracker'),
-      'description' => $this->t("A flexible SASS component to illustrate the steps in a multi step process e.g. a multi step form, a timeline or a quiz."),
+      'description' => $this->t("A flexible SASS component to illustrate the steps in a multi-step process e.g. a multi-step form, a timeline or a quiz."),
       'notes' => $this->t('Progress Tracker is used by multi-step wizard forms.'),
       'homepage_url' => Url::fromUri('http://nigelotoole.github.io/progress-tracker/'),
       'download_url' => Url::fromUri('https://github.com/NigelOToole/progress-tracker/archive/v1.4.0.zip'),
       'version' => '1.4.0',
-      'optional' => TRUE,
     ];
     $libraries['signature_pad'] = [
       'title' => $this->t('Signature Pad'),
@@ -446,10 +452,20 @@ protected function initLibraries() {
       'elements' => ['webform_signature'],
     ];
 
+    // Add webform as the provider to all libraries.
+    foreach ($libraries as $library_name => $library) {
+      $libraries[$library_name] += [
+        'optional' => TRUE,
+        'provider' => 'webform',
+      ];
+    }
+
     // Allow other modules to define webform libraries.
     foreach ($this->moduleHandler->getImplementations('webform_libraries_info') as $module) {
       foreach ($this->moduleHandler->invoke($module, 'webform_libraries_info') as $library_name => $library) {
-        $libraries[$library_name] = $library;
+        $libraries[$library_name] = $library + [
+          'provider' => $module,
+        ];
       }
     }
 
@@ -459,6 +475,14 @@ protected function initLibraries() {
     // Sort libraries by key.
     ksort($libraries);
 
+    // Move deprecated libraries last.
+    foreach ($libraries as $library_name => $library) {
+      if (!empty($library['deprecated'])) {
+        unset($libraries[$library_name]);
+        $libraries[$library_name] = $library;
+      }
+    }
+
     return $libraries;
   }
 
diff --git a/web/modules/webform/src/WebformMessageManager.php b/web/modules/webform/src/WebformMessageManager.php
index 29df9b49af7f1399b1a867a360537a084a4f9070..7e6b1f78744adf281ed71af6666668fa91ab95de 100644
--- a/web/modules/webform/src/WebformMessageManager.php
+++ b/web/modules/webform/src/WebformMessageManager.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\webform;
 
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Config\ConfigFactoryInterface;
@@ -96,6 +98,13 @@ class WebformMessageManager implements WebformMessageManagerInterface {
    */
   protected $webformSubmission;
 
+  /**
+   * The messenger.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
   /**
    * Constructs a WebformMessageManager object.
    *
@@ -104,22 +113,28 @@ class WebformMessageManager implements WebformMessageManagerInterface {
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The configuration object factory.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
-   *   The entity manager.
+   *   The entity type manager.
    * @param \Psr\Log\LoggerInterface $logger
    *   A logger instance.
    * @param \Drupal\Core\Render\RendererInterface $renderer
    *   The renderer.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger.
    * @param \Drupal\webform\WebformRequestInterface $request_handler
    *   The webform request handler.
    * @param \Drupal\webform\WebformTokenManagerInterface $token_manager
    *   The webform token manager.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
    */
-  public function __construct(AccountInterface $current_user, ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, LoggerInterface $logger, RendererInterface $renderer, WebformRequestInterface $request_handler, WebformTokenManagerInterface $token_manager) {
+  public function __construct(AccountInterface $current_user, ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, LoggerInterface $logger, RendererInterface $renderer, MessengerInterface $messenger, WebformRequestInterface $request_handler, WebformTokenManagerInterface $token_manager) {
     $this->currentUser = $current_user;
     $this->configFactory = $config_factory;
     $this->entityStorage = $entity_type_manager->getStorage('webform_submission');
     $this->logger = $logger;
     $this->renderer = $renderer;
+    $this->messenger = $messenger;
     $this->requestHandler = $request_handler;
     $this->tokenManager = $token_manager;
   }
@@ -171,7 +186,7 @@ public function append(array $build, $key, $type = 'status') {
    */
   public function display($key, $type = 'status') {
     if ($build = $this->build($key)) {
-      drupal_set_message($this->renderer->renderPlain($build), $type);
+      $this->messenger->addMessage($this->renderer->renderPlain($build), $type);
     }
   }
 
@@ -197,7 +212,7 @@ public function build($key) {
       }
 
       // Set max-age to 0 if settings message contains any [token] values.
-      $setting_message = $this->setting($key);
+      $setting_message = $this->getSetting($key);
       if ($setting_message && strpos($setting_message, '[') !== FALSE) {
         $message['#cache']['max-age'] = 0;
       }
@@ -209,97 +224,93 @@ public function build($key) {
     }
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setting($key) {
-    $webform_settings = ($this->webform) ? $this->webform->getSettings() : [];
-    if (!empty($webform_settings[$key])) {
-      return $webform_settings[$key];
-    }
-
-    $default_settings = $this->configFactory->get('webform.settings')->get('settings');
-    if (!empty($default_settings['default_' . $key])) {
-      return $default_settings['default_' . $key];
-    }
-    return FALSE;
-  }
-
   /**
    * {@inheritdoc}
    */
   public function get($key) {
-    // Get message from settings.
-    if ($setting = $this->setting($key)) {
-      $entity = $this->webformSubmission ?: $this->webform;
-      return WebformHtmlEditor::checkMarkup($this->tokenManager->replace($setting, $entity));
+    // Get custom message from settings.
+    if ($custom_message = $this->getCustomMessage($key)) {
+      return $custom_message;
     }
 
     $webform = $this->webform;
     $source_entity = $this->sourceEntity;
 
-    $t_args = [
-      '%form' => ($source_entity) ? $source_entity->label() : $webform->label(),
-      ':handlers_href' => $webform->toUrl('handlers')->toString(),
-      ':settings_href' => $webform->toUrl('settings')->toString(),
-      ':duplicate_href' => $webform->toUrl('duplicate-form')->toString(),
-    ];
+    // Get custom message from settings with arguments.
+    switch ($key) {
+      case WebformMessageManagerInterface::PREVIOUS_SUBMISSION:
+        $webform_submission = $this->entityStorage->getLastSubmission($webform, $source_entity, $this->currentUser);
+        $args = [':href' => $this->requestHandler->getUrl($webform_submission, $source_entity, 'webform.user.submission')->toString()];
+        return $this->getCustomMessage('previous_submission_message', $args);
 
+      case WebformMessageManagerInterface::PREVIOUS_SUBMISSIONS:
+        $args = [':href' => $this->requestHandler->getUrl($webform, $source_entity, 'webform.user.submissions')->toString()];
+        return $this->getCustomMessage('previous_submissions_message', $args);
+    }
+
+    // Get hard-coded messages.
     switch ($key) {
       case WebformMessageManagerInterface::ADMIN_PAGE:
-        return $this->t('Only webform administrators are allowed to access this page and create new submissions.', $t_args);
+        return $this->t('Only webform administrators are allowed to access this page and create new submissions.');
 
       case WebformMessageManagerInterface::ADMIN_CLOSED:
-        return $this->t('This webform is <a href=":settings_href">closed</a>. Only submission administrators are allowed to access this webform and create new submissions.', $t_args);
+        $t_args = [':href' => $webform->toUrl('settings-form')->toString()];
+        return $this->t('This webform is <a href=":href">closed</a>. Only submission administrators are allowed to access this webform and create new submissions.', $t_args);
+
+      case WebformMessageManagerInterface::ADMIN_ARCHIVED:
+        $t_args = [':href' => $webform->toUrl('settings')->toString()];
+        return $this->t('This webform is <a href=":href">archived</a>. Only submission administrators are allowed to access this webform and create new submissions.', $t_args);
 
       case WebformMessageManagerInterface::SUBMISSION_DEFAULT_CONFIRMATION:
+        $t_args = ['%form' => ($source_entity) ? $source_entity->label() : $webform->label()];
         return $this->t('New submission added to %form.', $t_args);
 
       case WebformMessageManagerInterface::FORM_SAVE_EXCEPTION:
+        $t_args = [
+          ':handlers_href' => $webform->toUrl('handlers')->toString(),
+          ':settings_href' => $webform->toUrl('settings')->toString(),
+        ];
         return $this->t('This webform is currently not saving any submitted data. Please enable the <a href=":settings_href">saving of results</a> or add a <a href=":handlers_href">submission handler</a> to the webform.', $t_args);
 
       case WebformMessageManagerInterface::HANDLER_SUBMISSION_REQUIRED:
-        return $this->t('This webform\'s <a href=":handlers_href">submission handlers</a> requires submissions to be saved to the database.', $t_args);
-
-      case WebformMessageManagerInterface::SUBMISSION_PREVIOUS:
-        $webform_submission = $this->entityStorage->getLastSubmission($webform, $source_entity, $this->currentUser);
-        $t_args[':submission_href'] = $this->requestHandler->getUrl($webform_submission, $source_entity, 'webform.user.submission')->toString();
-        return $this->t('You have already submitted this webform.') . ' ' . $this->t('<a href=":submission_href">View your previous submission</a>.', $t_args);
-
-      case WebformMessageManagerInterface::SUBMISSIONS_PREVIOUS:
-        $t_args[':submissions_href'] = $this->requestHandler->getUrl($webform, $source_entity, 'webform.user.submissions')->toString();
-        return $this->t('You have already submitted this webform.') . ' ' . $this->t('<a href=":submissions_href">View your previous submissions</a>.', $t_args);
+        $t_args = [':href' => $webform->toUrl('handlers')->toString()];
+        return $this->t('This webform\'s <a href=":href">submission handlers</a> requires submissions to be saved to the database.', $t_args);
 
       case WebformMessageManagerInterface::DRAFT_PREVIOUS:
         $webform_draft = $this->entityStorage->loadDraft($webform, $source_entity, $this->currentUser);
-        if ($source_entity && $source_entity->hasLinkTemplate('canonical')) {
-          $t_args[':draft_href'] = $source_entity->toUrl('canonical', ['query' => ['token' => $webform_draft->getToken()]])->toString();
-        }
-        else {
-          $t_args[':draft_href'] = $webform->toUrl('canonical', ['query' => ['token' => $webform_draft->getToken()]])->toString();
-        }
-        return $this->t('You have a pending draft for this webform.') . ' ' . $this->t('<a href=":draft_href">Load your pending draft</a>.', $t_args);
+        $t_args = [':href' => $webform_draft->getTokenUrl()->toString()];
+        return $this->t('You have a pending draft for this webform.') . ' ' . $this->t('<a href=":href">Load your pending draft</a>.', $t_args);
 
       case WebformMessageManagerInterface::DRAFTS_PREVIOUS:
-        $t_args[':drafts_href'] = $this->requestHandler->getUrl($webform, $source_entity, 'webform.user.drafts')->toString();
-        return $this->t('You have pending drafts for this webform.') . ' ' . $this->t('<a href=":drafts_href">View your pending drafts</a>.', $t_args);
+        $t_args = [':href' => $this->requestHandler->getUrl($webform, $source_entity, 'webform.user.drafts')->toString()];
+        return $this->t('You have pending drafts for this webform.') . ' ' . $this->t('<a href=":href">View your pending drafts</a>.', $t_args);
 
       case WebformMessageManagerInterface::SUBMISSION_UPDATED:
+        $t_args = ['%form' => ($source_entity) ? $source_entity->label() : $webform->label()];
         return $this->t('Submission updated in %form.', $t_args);
 
       case WebformMessageManagerInterface::SUBMISSION_TEST:
-        return $this->t("The below webform has been prepopulated with custom/random test data. When submitted, this information <strong>will still be saved</strong> and/or <strong>sent to designated recipients</strong>.", $t_args);
+        return $this->t("The below webform has been prepopulated with custom/random test data. When submitted, this information <strong>will still be saved</strong> and/or <strong>sent to designated recipients</strong>.");
 
       case WebformMessageManagerInterface::TEMPLATE_PREVIEW:
-        return $this->t('You are previewing the below template, which can be used to <a href=":duplicate_href">create a new webform</a>. <strong>Submitted data will be ignored</strong>.', $t_args);
+        $t_args = [':href' => $webform->toUrl('duplicate-form')->toString()];
+        return $this->t('You are previewing the below template, which can be used to <a href=":href">create a new webform</a>. <strong>Submitted data will be ignored</strong>.', $t_args);
 
       case WebformMessageManagerInterface::PREPOPULATE_SOURCE_ENTITY_TYPE:
       case WebformMessageManagerInterface::PREPOPULATE_SOURCE_ENTITY_REQUIRED:
-        return $this->t('This webform is not available. Please contact the site administrator.', $t_args);
+        return $this->t('This webform is not available. Please contact the site administrator.');
 
-      default:
-        return FALSE;
+      case WebformMessageManagerInterface::PREVIOUS_SUBMISSION:
+        $webform_submission = $this->entityStorage->getLastSubmission($webform, $source_entity, $this->currentUser);
+        $args = [':href' => $this->requestHandler->getUrl($webform_submission, $source_entity, 'webform.user.submission')->toString()];
+        return $this->getCustomMessage('previous_submission_message', $args);
+
+      case WebformMessageManagerInterface::PREVIOUS_SUBMISSIONS:
+        $args = [':href' => $this->requestHandler->getUrl($webform, $source_entity, 'webform.user.submissions')->toString()];
+        return $this->getCustomMessage('previous_submissions_message', $args);
     }
+
+    return FALSE;
   }
 
   /**
@@ -336,4 +347,56 @@ public function log($key, $type = 'warning') {
     $this->logger->$type($message, $context);
   }
 
+  /**
+   * Get message from webform specific setting or global setting.
+   *
+   * @param string $key
+   *   The name of webform settings message to be displayed.
+   *
+   * @return string|bool
+   *   A message or FALSE if no message is found.
+   */
+  protected function getSetting($key) {
+    $webform_settings = ($this->webform) ? $this->webform->getSettings() : [];
+    if (!empty($webform_settings[$key])) {
+      return $webform_settings[$key];
+    }
+
+    $default_settings = $this->configFactory->get('webform.settings')->get('settings');
+    if (!empty($default_settings['default_' . $key])) {
+      return $default_settings['default_' . $key];
+    }
+    return FALSE;
+  }
+
+  /**
+   * Get custom message.
+   *
+   * @param string $key
+   *   Message key.
+   * @param array $arguments
+   *   An array with placeholder replacements, keyed by placeholder.
+   *
+   * @return array|bool
+   *   Renderable array or FALSE if custom message does not exist.
+   */
+  protected function getCustomMessage($key, array $arguments = []) {
+    $setting = $this->getSetting($key);
+    if (!$setting) {
+      return FALSE;
+    }
+
+    // Replace tokens.
+    $entity = $this->webformSubmission ?: $this->webform;
+    $message = $this->tokenManager->replace($setting, $entity);
+
+    // Replace arguments.
+    if ($arguments) {
+      $message = str_replace('href="#"', 'href=":href"', $message);
+      $message = new FormattableMarkup($message, $arguments);
+    }
+
+    return WebformHtmlEditor::checkMarkup($message);
+  }
+
 }
diff --git a/web/modules/webform/src/WebformMessageManagerInterface.php b/web/modules/webform/src/WebformMessageManagerInterface.php
index c8657c2589ad4d00dba7ccc6f92308cf805ded87..5712adc00370539705f0413a895de1e5fd4f54df 100644
--- a/web/modules/webform/src/WebformMessageManagerInterface.php
+++ b/web/modules/webform/src/WebformMessageManagerInterface.php
@@ -10,78 +10,83 @@
 interface WebformMessageManagerInterface {
 
   /****************************************************************************/
-  // Hardcode message constants.
+  // Hardcode message or custom messages with arguments constants.
   /****************************************************************************/
 
   /**
    * Admin closed.
    */
-  const ADMIN_CLOSED = 1;
+  const ADMIN_CLOSED = 'admin_closed';
 
   /**
    * Admin page.
    */
-  const ADMIN_PAGE = 2;
+  const ADMIN_PAGE = 'admin_page';
+
+  /**
+   * Admin archived.
+   */
+  const ADMIN_ARCHIVED = 'admin_archived';
 
   /**
    * Default submission confirmation.
    */
-  const SUBMISSION_DEFAULT_CONFIRMATION = 3;
+  const SUBMISSION_DEFAULT_CONFIRMATION = 'submission_default_confirmation';
 
   /**
    * Submission previous.
    */
-  const SUBMISSION_PREVIOUS = 4;
+  const PREVIOUS_SUBMISSION = 'previous_submission';
 
   /**
    * Submissions previous.
    */
-  const SUBMISSIONS_PREVIOUS = 5;
+  const PREVIOUS_SUBMISSIONS = 'previous_submissions';
 
   /**
    * Submission updates.
    */
-  const SUBMISSION_UPDATED = 6;
+  const SUBMISSION_UPDATED = 'submission_updated';
 
   /**
    * Submission test.
    */
-  const SUBMISSION_TEST = 7;
+  const SUBMISSION_TEST = 'submission_test';
 
   /**
    * Webform not saving or sending any data.
    */
-  const FORM_SAVE_EXCEPTION = 8;
+  const FORM_SAVE_EXCEPTION = 'form_save_exception';
 
   /**
    * Webform not able to handle file uploads.
    */
-  const FORM_FILE_UPLOAD_EXCEPTION = 9;
+  const FORM_FILE_UPLOAD_EXCEPTION = 'form_file_upload_exception';
 
   /**
    * Handler submission test.
    */
-  const HANDLER_SUBMISSION_REQUIRED = 10;
+  const HANDLER_SUBMISSION_REQUIRED = 'handler_submission_required';
 
   /**
    * Draft previous.
    */
-  const DRAFT_PREVIOUS = 11;
+  const DRAFT_PREVIOUS = 'draft_previous';
 
   /**
    * Drafts previous.
    */
-  const DRAFTS_PREVIOUS = 12;
+  const DRAFTS_PREVIOUS = 'drafts_previous';
 
   /****************************************************************************/
-  // Configurable message constants.
+  // Configurable custom message constants.
   // Values corresponds to admin config and webform settings.
   /****************************************************************************/
 
   /**
    * Webform exception.
    */
-  const FORM_EXCEPTION = 'form_exception_message';
+  const FORM_EXCEPTION_MESSAGE = 'form_exception_message';
 
   /**
    * Webform preview.
@@ -116,33 +121,38 @@ interface WebformMessageManagerInterface {
   /**
    * Submission draft saved.
    */
-  const SUBMISSION_DRAFT_SAVED = 'draft_saved_message';
+  const SUBMISSION_DRAFT_SAVED_MESSAGE = 'draft_saved_message';
 
   /**
    * Submission draft loaded.
    */
-  const SUBMISSION_DRAFT_LOADED = 'draft_loaded_message';
+  const SUBMISSION_DRAFT_LOADED_MESSAGE = 'draft_loaded_message';
 
   /**
    * Submission confirmation.
    */
-  const SUBMISSION_CONFIRMATION = 'confirmation_message';
+  const SUBMISSION_CONFIRMATION_MESSAGE = 'confirmation_message';
 
   /**
    * Submission exception.
    */
-  const SUBMISSION_EXCEPTION = 'submission_exception_message';
+  const SUBMISSION_EXCEPTION_MESSAGE = 'submission_exception_message';
 
   /**
    * Submission exception.
    */
-  const SUBMISSION_LOCKED = 'submission_locked_message';
+  const SUBMISSION_LOCKED_MESSAGE = 'submission_locked_message';
 
   /**
    * Template preview.
    */
   const TEMPLATE_PREVIEW = 'template_preview';
 
+  /**
+   * Autofill.
+   */
+  const AUTOFILL_MESSAGE = 'autofill_message';
+
   /**
    * Prepopulate source entity required.
    */
@@ -153,11 +163,6 @@ interface WebformMessageManagerInterface {
    */
   const PREPOPULATE_SOURCE_ENTITY_TYPE = 'prepopulate_source_entity_type';
 
-  /**
-   * Autofill.
-   */
-  const AUTOFILL = 'autofill_message';
-
   /**
    * Set the webform submission used for token replacement.
    *
@@ -184,17 +189,6 @@ public function setWebform(WebformInterface $webform = NULL);
    */
   public function setSourceEntity(EntityInterface $entity = NULL);
 
-  /**
-   * Get message from webform specific setting or global setting.
-   *
-   * @param string $key
-   *   The name of webform settings message to be displayed.
-   *
-   * @return string|bool
-   *   A message or FALSE if no message is found.
-   */
-  public function setting($key);
-
   /**
    * Get message.
    *
diff --git a/web/modules/webform/src/WebformOptionsAccessControlHandler.php b/web/modules/webform/src/WebformOptionsAccessControlHandler.php
index 670399c13ac2d02dcc8625d310f39e23e37e81de..7952efe6356d4caada5a8e854613b641ec90ef6c 100644
--- a/web/modules/webform/src/WebformOptionsAccessControlHandler.php
+++ b/web/modules/webform/src/WebformOptionsAccessControlHandler.php
@@ -18,7 +18,7 @@ class WebformOptionsAccessControlHandler extends EntityAccessControlHandler {
    * {@inheritdoc}
    */
   public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
-    return AccessResult::allowedIf($account->hasPermission('administer webform'))->cachePerPermissions();
+    return AccessResult::allowedIfHasPermission($account, 'administer webform');
   }
 
 }
diff --git a/web/modules/webform/src/WebformOptionsDeleteForm.php b/web/modules/webform/src/WebformOptionsDeleteForm.php
index 5215faf5fc13ad2b6e48002006ce5bc8ea4266ae..e9ce67db7fbeec3457c2b855789c109887c88f26 100644
--- a/web/modules/webform/src/WebformOptionsDeleteForm.php
+++ b/web/modules/webform/src/WebformOptionsDeleteForm.php
@@ -2,72 +2,79 @@
 
 namespace Drupal\webform;
 
-use Drupal\Core\Entity\EntityDeleteForm;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Url;
-use Drupal\webform\Form\WebformDialogFormTrait;
+use Drupal\webform\Form\WebformConfigEntityDeleteFormBase;
 
 /**
  * Provides a delete webform options form.
  */
-class WebformOptionsDeleteForm extends EntityDeleteForm {
-
-  use WebformDialogFormTrait;
+class WebformOptionsDeleteForm extends WebformConfigEntityDeleteFormBase {
 
   /**
    * {@inheritdoc}
    */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $form = parent::buildForm($form, $form_state);
+  public function getDescription() {
+    return [
+      'title' => [
+        '#markup' => $this->t('This action will…'),
+      ],
+      'list' => [
+        '#theme' => 'item_list',
+        '#items' => [
+          $this->t('Remove configuration'),
+          $this->t('Affect any elements which use these options'),
+        ],
+      ],
+    ];
+  }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getDetails() {
     /** @var \Drupal\webform\WebformOptionsInterface $webform_options */
     $webform_options = $this->entity;
 
     /** @var \Drupal\webform\WebformOptionsStorageInterface $webform_options_storage */
     $webform_options_storage = $this->entityTypeManager->getStorage('webform_options');
 
-    // Display warning that options is used by composite elements
-    // and/or webforms.
-    $t_args = ['%title' => $webform_options->label()];
-    $message = [];
+    $t_args = [
+      '%label' => $this->getEntity()->label(),
+      '@entity-type' => $this->getEntity()->getEntityType()->getLowercaseLabel(),
+    ];
+
+    $details = [];
     if ($used_by_elements = $webform_options_storage->getUsedByCompositeElements($webform_options)) {
-      $message['elements'] = [
-        '#theme' => 'item_list',
-        '#title' => $this->t('%title is used by the below composite element(s).', $t_args),
-        '#items' => $used_by_elements,
+      $details['elements'] = [
+        'title' => [
+          '#markup' => $this->t('%label is used by the below composite element(s).', $t_args),
+        ],
+        'list' => [
+          '#theme' => 'item_list',
+          '#items' => $used_by_elements,
+        ],
       ];
     }
     if ($used_by_webforms = $webform_options_storage->getUsedByWebforms($webform_options)) {
-      $message['webform'] = [
-        '#theme' => 'item_list',
-        '#title' => $this->t('%title is used by the below webform(s).', $t_args),
-        '#items' => $used_by_webforms,
+      $details['webform'] = [
+        'title' => [
+          '#markup' => $this->t('%label is used by the below webform(s).', $t_args),
+        ],
+        'list' => [
+          '#theme' => 'item_list',
+          '#items' => $used_by_webforms,
+        ],
       ];
     }
-    if ($message) {
-      $form['used_by_composite_elements'] = [
-        '#type' => 'webform_message',
-        '#message_message' => $message,
-        '#message_type' => 'warning',
-        '#weight' => -100,
-      ];
-    }
-
-    $form['confirm'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Yes, I want to delete these webform options.'),
-      '#required' => TRUE,
-      '#weight' => 10,
-    ];
-
-    return $this->buildDialogConfirmForm($form, $form_state);
-  }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getRedirectUrl() {
-    return Url::fromRoute('entity.webform_options.collection');
+    if ($details) {
+      return [
+        '#type' => 'details',
+        '#title' => $this->t('Webforms affected'),
+      ] + $details;
+    }
+    else {
+      return [];
+    }
   }
 
 }
diff --git a/web/modules/webform/src/WebformOptionsForm.php b/web/modules/webform/src/WebformOptionsForm.php
index f93a6a2aae5de6e2662e9beaa8bafa5ffb1c7589..66db47b221f41518d1494f16debe186e5da599b7 100644
--- a/web/modules/webform/src/WebformOptionsForm.php
+++ b/web/modules/webform/src/WebformOptionsForm.php
@@ -8,6 +8,7 @@
 use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
 use Drupal\webform\Entity\WebformOptions;
 use Drupal\webform\Utility\WebformArrayHelper;
+use Drupal\webform\Utility\WebformDialogHelper;
 use Drupal\webform\Utility\WebformOptionsHelper;
 
 /**
@@ -30,7 +31,7 @@ protected function prepareEntity() {
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
-    /** @var \Drupal\webform\WebformOptionsInterface $webform */
+    /** @var \Drupal\webform\WebformOptionsInterface $webform_options */
     $webform_options = $this->getEntity();
 
     // Customize title for duplicate and edit operation.
@@ -40,6 +41,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         break;
 
       case 'edit':
+      case 'source':
         $form['#title'] = $webform_options->label();
         break;
     }
@@ -102,10 +104,10 @@ public function form(array $form, FormStateInterface $form_state) {
         '@module' => new PluralTranslatableMarkup(count($module_names), $this->t('module'), $this->t('modules')),
       ];
       if (empty($webform_options->get('options'))) {
-        drupal_set_message($this->t('The %title options are being set by the %module_names @module. Altering any of the below options will override these dynamically populated options.', $t_args), 'warning');
+        $this->messenger()->addWarning($this->t('The %title options are being set by the %module_names @module. Altering any of the below options will override these dynamically populated options.', $t_args));
       }
       else {
-        drupal_set_message($this->t('The %title options have been customized. Resetting the below options will allow the %module_names @module to dynamically populate these options.', $t_args), 'warning');
+        $this->messenger()->addWarning($this->t('The %title options have been customized. Resetting the below options will allow the %module_names @module to dynamically populate these options.', $t_args));
       }
     }
 
@@ -134,6 +136,12 @@ protected function actions(array $form, FormStateInterface $form_state) {
       ];
     }
 
+    // Open delete button in a modal dialog.
+    if (isset($actions['delete'])) {
+      $actions['delete']['#attributes'] = WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW, $actions['delete']['#attributes']['class']);
+      WebformDialogHelper::attachLibraries($actions['delete']);
+    }
+
     return $actions;
   }
 
@@ -225,7 +233,7 @@ public function reset(array &$form, FormStateInterface $form_state) {
     ];
     $this->logger('webform')->notice('Options @label have been reset.', $context);
 
-    drupal_set_message($this->t('Options %label have been reset.', ['%label' => $webform_options->label()]));
+    $this->messenger()->addStatus($this->t('Options %label have been reset.', ['%label' => $webform_options->label()]));
 
     $form_state->setRedirect('entity.webform_options.collection');
   }
@@ -244,7 +252,7 @@ public function save(array $form, FormStateInterface $form_state) {
     ];
     $this->logger('webform')->notice('Options @label saved.', $context);
 
-    drupal_set_message($this->t('Options %label saved.', ['%label' => $webform_options->label()]));
+    $this->messenger()->addStatus($this->t('Options %label saved.', ['%label' => $webform_options->label()]));
 
     $form_state->setRedirect('entity.webform_options.collection');
   }
diff --git a/web/modules/webform/src/WebformOptionsListBuilder.php b/web/modules/webform/src/WebformOptionsListBuilder.php
index c10a9c90dc94135ca52167a31935c764070f0f2a..6f32a5cb01619a673eb8731d1fa9a6598f19baef 100644
--- a/web/modules/webform/src/WebformOptionsListBuilder.php
+++ b/web/modules/webform/src/WebformOptionsListBuilder.php
@@ -4,10 +4,15 @@
 
 use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Form\OptGroup;
 use Drupal\Core\Url;
 use Drupal\webform\Entity\WebformOptions;
 use Drupal\webform\Utility\WebformDialogHelper;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Defines a class to build a listing of webform options entities.
@@ -16,28 +21,108 @@
  */
 class WebformOptionsListBuilder extends ConfigEntityListBuilder {
 
+  /**
+   * Search keys.
+   *
+   * @var string
+   */
+  protected $keys;
+
+  /**
+   * Search category.
+   *
+   * @var string
+   */
+  protected $category;
+
+  /**
+   * Constructs a new WebformOptionsListBuilder object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   *   The entity storage class.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   */
+  public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, RequestStack $request_stack) {
+    parent::__construct($entity_type, $storage);
+    $this->request = $request_stack->getCurrentRequest();
+
+    $this->keys = $this->request->query->get('search');
+    $this->category = $this->request->query->get('category');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('entity.manager')->getStorage($entity_type->id()),
+      $container->get('request_stack')
+    );
+  }
+
   /**
    * {@inheritdoc}
    */
   public function render() {
+    // Handler autocomplete redirect.
+    if ($this->keys && preg_match('#\(([^)]+)\)$#', $this->keys, $match)) {
+      if ($webform_options = $this->getStorage()->load($match[1])) {
+        return new RedirectResponse($webform_options->toUrl()->setAbsolute(TRUE)->toString());
+      }
+    }
+
     $build = [];
 
+    // Filter form.
+    $build['filter_form'] = $this->buildFilterForm();
+
     // Display info.
-    if ($total = $this->getStorage()->getQuery()->count()->execute()) {
-      $build['info'] = [
-        '#markup' => $this->formatPlural($total, '@total option', '@total options', ['@total' => $total]),
-        '#prefix' => '<div>',
-        '#suffix' => '</div>',
-      ];
-    }
+    $build['info'] = $this->buildInfo();
 
+    // Table.
     $build += parent::render();
+    $build['table']['#sticky'] = TRUE;
 
+    // Attachments.
     $build['#attached']['library'][] = 'webform/webform.admin.dialog';
 
     return $build;
   }
 
+  /**
+   * Build the filter form.
+   *
+   * @return array
+   *   A render array representing the filter form.
+   */
+  protected function buildFilterForm() {
+    $categories = $this->getStorage()->getCategories();
+    return \Drupal::formBuilder()->getForm('\Drupal\webform\Form\WebformOptionsFilterForm', $this->keys, $this->category, $categories);
+  }
+
+  /**
+   * Build information summary.
+   *
+   * @return array
+   *   A render array representing the information summary.
+   */
+  protected function buildInfo() {
+    $total = $this->getQuery($this->keys, $this->category)->count()->execute();
+    if (!$total) {
+      return [];
+    }
+
+    return [
+      '#markup' => $this->formatPlural($total, '@total option', '@total options', ['@total' => $total]),
+      '#prefix' => '<div>',
+      '#suffix' => '</div>',
+    ];
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -96,7 +181,7 @@ public function getDefaultOperations(EntityInterface $entity, $type = 'edit') {
    * Build list of webforms and composite elements that the webform options is used by.
    *
    * @param \Drupal\webform\WebformOptionsInterface $webform_options
-   *   A webform options entity
+   *   A webform options entity.
    *
    * @return array
    *   Table data containing list of webforms and composite elements that the
@@ -110,14 +195,14 @@ protected function buildUsedBy(WebformOptionsInterface $webform_options) {
         '#type' => 'link',
         '#title' => $title,
         '#url' => Url::fromRoute('entity.webform.canonical', ['webform' => $id]),
-        '#suffix' => '</br>'
+        '#suffix' => '</br>',
       ];
     }
     $elements = $this->getStorage()->getUsedByCompositeElements($webform_options);
     foreach ($elements as $id => $title) {
       $links[] = [
         '#markup' => $title,
-        '#suffix' => '</br>'
+        '#suffix' => '</br>',
       ];
     }
     return [
@@ -130,7 +215,7 @@ protected function buildUsedBy(WebformOptionsInterface $webform_options) {
    * Build list of webform options.
    *
    * @param \Drupal\webform\WebformOptionsInterface $webform_options
-   *   A webform options entity
+   *   A webform options entity.
    *
    * @return string
    *   Semi-colon delimited list of webform options.
@@ -144,7 +229,59 @@ protected function buildOptions(WebformOptionsInterface $webform_options) {
         $value .= ' (' . $key . ')';
       }
     }
-    return implode('; ', array_slice($options, 0, 12)) . (count($options) > 12 ? '; ...' : '');
+    return implode('; ', array_slice($options, 0, 12)) . (count($options) > 12 ? '; …' : '');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOperations(EntityInterface $entity) {
+    return parent::buildOperations($entity) + [
+        '#prefix' => '<div class="webform-dropbutton">',
+        '#suffix' => '</div>',
+      ];
+  }
+
+  /**
+   * Get the base entity query filtered by search and category.
+   *
+   * @param string $keys
+   *   (optional) Search key.
+   * @param string $category
+   *   (optional) Category.
+   *
+   * @return \Drupal\Core\Entity\Query\QueryInterface
+   *   An entity query.
+   */
+  protected function getQuery($keys = '', $category = '') {
+    $query = $this->getStorage()->getQuery();
+
+    // Filter by key(word).
+    if ($keys) {
+      $or = $query->orConditionGroup()
+        ->condition('id', $keys, 'CONTAINS')
+        ->condition('title', $keys, 'CONTAINS')
+        ->condition('options', $keys, 'CONTAINS');
+      $query->condition($or);
+    }
+
+    // Filter by category.
+    if ($category) {
+      $query->condition('category', $category);
+    }
+
+    return $query;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntityIds() {
+    $header = $this->buildHeader();
+    $query = $this->getQuery($this->keys, $this->category);
+    $query->tableSort($header);
+    $query->pager($this->limit);
+    return $query->execute();
   }
 
 }
diff --git a/web/modules/webform/src/WebformOptionsStorage.php b/web/modules/webform/src/WebformOptionsStorage.php
index 3ed0f9ad41dc2f0f768f2376672263dd5c1fcd36..ea5066c3b241912a3c33b8208671fc7cd4105771 100644
--- a/web/modules/webform/src/WebformOptionsStorage.php
+++ b/web/modules/webform/src/WebformOptionsStorage.php
@@ -3,13 +3,13 @@
 namespace Drupal\webform;
 
 use Drupal\Component\Render\FormattableMarkup;
-use Drupal\Component\Serialization\Yaml;
 use Drupal\Component\Uuid\UuidInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Config\Entity\ConfigEntityStorage;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Render\ElementInfoManagerInterface;
+use Drupal\Core\Serialization\Yaml;
 use Drupal\webform\Element\WebformCompositeBase;
 use Drupal\webform\Plugin\WebformElementManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -144,7 +144,9 @@ public function getUsedByCompositeElements(WebformOptionsInterface $webform_opti
       foreach (array_keys($definitions) as $plugin_id) {
         /** @var \Drupal\Core\Render\Element\ElementInterface $element */
         $element = $this->elementInfo->createInstance($plugin_id);
-        if (!$element instanceof WebformCompositeBase) {
+        // Make sure element is composite and not provided by the
+        // webform_composite.module.
+        if (!$element instanceof WebformCompositeBase || in_array($plugin_id, ['webform_composite'])) {
           continue;
         }
 
@@ -191,8 +193,8 @@ public function getUsedByWebforms(WebformOptionsInterface $webform_options) {
         $config = $this->configFactory->get($webform_config_name);
         $element_data = Yaml::encode($config->get('elements'));
         if (preg_match_all('/(?:options|answers)\'\: ([a-z_]+)/', $element_data, $matches)) {
-          $webform_id  = $config->get('id');
-          $webform_title  = $config->get('title');
+          $webform_id = $config->get('id');
+          $webform_title = $config->get('title');
           foreach ($matches[1] as $options_id) {
             $this->usedByWebforms[$options_id][$webform_id] = $webform_title;
           }
diff --git a/web/modules/webform/src/WebformRequest.php b/web/modules/webform/src/WebformRequest.php
index 3261a35d25c2566d36e4be08a224a0f6f392932b..a848bfa1503400cc22fe0beb18761c660044d155 100644
--- a/web/modules/webform/src/WebformRequest.php
+++ b/web/modules/webform/src/WebformRequest.php
@@ -93,7 +93,7 @@ class WebformRequest implements WebformRequestInterface {
    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
    *   The current route match.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
-   *   The entity type repository.
+   *   The entity type manager.
    * @param \Drupal\Core\Entity\EntityTypeRepositoryInterface $entity_type_repository
    *   The entity type repository.
    * @param \Drupal\webform\WebformEntityReferenceManagerInterface $webform_entity_reference_manager
@@ -172,6 +172,24 @@ public function getCurrentWebformSubmission() {
     return $webform_submission;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCurrentWebformUrl($route_name, array $route_options = []) {
+    $webform_entity = $this->getCurrentWebform();
+    $source_entity = $this->getCurrentSourceEntity();
+    return $this->getUrl($webform_entity, $source_entity, $route_name, $route_options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCurrentWebformSubmissionUrl($route_name, array $route_options = []) {
+    $webform_entity = $this->getCurrentWebformSubmission();
+    $source_entity = $this->getCurrentSourceEntity();
+    return $this->getUrl($webform_entity, $source_entity, $route_name, $route_options);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/web/modules/webform/src/WebformRequestInterface.php b/web/modules/webform/src/WebformRequestInterface.php
index eea30d5de62f297da79eec7d54712ced5b9e99de..2f2f94c6408c692cde55f0764028a531d3173fdf 100644
--- a/web/modules/webform/src/WebformRequestInterface.php
+++ b/web/modules/webform/src/WebformRequestInterface.php
@@ -44,6 +44,32 @@ public function getCurrentWebform();
    */
   public function getCurrentWebformSubmission();
 
+  /**
+   * Get the URL for the current webform and source entity.
+   *
+   * @param string $route_name
+   *   The route name.
+   * @param array $route_options
+   *   The route options.
+   *
+   * @return \Drupal\Core\Url
+   *   The URL for a form/submission and source entity.
+   */
+  public function getCurrentWebformUrl($route_name, array $route_options = []);
+
+  /**
+   * Get the URL for the current webform submission and source entity.
+   *
+   * @param string $route_name
+   *   The route name.
+   * @param array $route_options
+   *   The route options.
+   *
+   * @return \Drupal\Core\Url
+   *   The URL for a form/submission and source entity.
+   */
+  public function getCurrentWebformSubmissionUrl($route_name, array $route_options = []);
+
   /**
    * Get the webform and source entity for the current request.
    *
diff --git a/web/modules/webform/src/WebformSubmissionAccessControlHandler.php b/web/modules/webform/src/WebformSubmissionAccessControlHandler.php
index 9c736fe3b5cef3d907701eb74674ff85e9f27adc..defea601a2c8304193fdc64fc55bfc435ae6ea41 100644
--- a/web/modules/webform/src/WebformSubmissionAccessControlHandler.php
+++ b/web/modules/webform/src/WebformSubmissionAccessControlHandler.php
@@ -2,17 +2,51 @@
 
 namespace Drupal\webform;
 
-use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityHandlerInterface;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\webform\Access\WebformAccessResult;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines the access control handler for the webform submission entity type.
  *
  * @see \Drupal\webform\Entity\WebformSubmission.
  */
-class WebformSubmissionAccessControlHandler extends EntityAccessControlHandler {
+class WebformSubmissionAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface {
+
+  /**
+   * Webform access rules manager service.
+   *
+   * @var \Drupal\webform\WebformAccessRulesManagerInterface
+   */
+  protected $accessRulesManager;
+
+  /**
+   * WebformSubmissionAccessControlHandler constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\webform\WebformAccessRulesManagerInterface $access_rules_manager
+   *   Webform access rules manager service.
+   */
+  public function __construct(EntityTypeInterface $entity_type, WebformAccessRulesManagerInterface $access_rules_manager) {
+    parent::__construct($entity_type);
+
+    $this->accessRulesManager = $access_rules_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('webform.access_rules_manager')
+    );
+  }
 
   /**
    * {@inheritdoc}
@@ -20,46 +54,41 @@ class WebformSubmissionAccessControlHandler extends EntityAccessControlHandler {
   public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
     /** @var \Drupal\webform\WebformSubmissionInterface $entity */
 
-    // Check webform submission access permissions.
-    // @todo: Refactor and consolidate below code after there are tests.
-    switch ($operation) {
-      case 'view':
-        // Allow users with 'view any webform submission' to view all submissions.
-        if ($account->hasPermission('view any webform submission')) {
-          return AccessResult::allowed();
-        }
+    // Check 'administer webform' permission.
+    if ($account->hasPermission('administer webform')) {
+      return WebformAccessResult::allowed();
+    }
 
-        // Allow users with 'view own webform submission' to view own submission.
-        if ($account->hasPermission('view own webform submission') && $entity->getOwnerId() == $account->id()) {
-          return AccessResult::allowed();
-        }
-        break;
+    // Check 'administer webform submission' permission.
+    if ($account->hasPermission('administer webform submission')) {
+      return WebformAccessResult::allowed();
+    }
 
-      case 'update':
-        // Allow users with 'edit any webform submission' to edit all submissions.
-        if ($account->hasPermission('edit any webform submission')) {
-          return AccessResult::allowed();
-        }
-        // Allow users with 'edit own webform submission' to edit own submission.
-        if ($account->hasPermission('edit own webform submission') && $entity->getOwnerId() == $account->id()) {
-          return AccessResult::allowed();
-        }
-        break;
+    // Check webform 'update' permission.
+    if ($entity->getWebform()->access('update', $account)) {
+      return WebformAccessResult::allowed($entity, TRUE);
+    }
 
-      case 'delete':
-        // Allow users with 'delete any webform submission' to edit all submissions.
-        if ($account->hasPermission('delete any webform submission')) {
-          return AccessResult::allowed();
-        }
-        // Allow users with 'delete own webform submission' to edit own submission.
-        if ($account->hasPermission('delete own webform submission') && $entity->getOwnerId() == $account->id()) {
-          return AccessResult::allowed();
-        }
-        break;
+    // Check 'any' or 'own' webform submission permissions.
+    $operations = [
+      'view' => 'view',
+      'update' => 'edit',
+      'delete' => 'delete',
+    ];
+    if (isset($operations[$operation])) {
+      $action = $operations[$operation];
+      // Check operation any.
+      if ($account->hasPermission("$action any webform submission")) {
+        return WebformAccessResult::allowed();
+      }
+      // Check operation own.
+      if ($account->hasPermission("$action own webform submission") && $entity->isOwner($account)) {
+        return WebformAccessResult::allowed($entity, TRUE);
+      }
     }
 
-    // Check webform update access.
-    $webform_access = $entity->getWebform()->checkAccessRules($operation, $account, $entity);
+    // Check webform access rules.
+    $webform_access = $this->accessRulesManager->checkWebformSubmissionAccess($operation, $account, $entity);
     if ($webform_access->isAllowed()) {
       return $webform_access;
     }
diff --git a/web/modules/webform/src/WebformSubmissionConditionsValidator.php b/web/modules/webform/src/WebformSubmissionConditionsValidator.php
index 71a2bb70c335033503b9dab20f3c33ebf91c3712..b75a82af1c12f18196bebb245eb51818487e8397 100644
--- a/web/modules/webform/src/WebformSubmissionConditionsValidator.php
+++ b/web/modules/webform/src/WebformSubmissionConditionsValidator.php
@@ -3,8 +3,9 @@
 namespace Drupal\webform;
 
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Render\Element;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\webform\Plugin\WebformElement\WebformCompositeBase;
+use Drupal\webform\Plugin\WebformElement\WebformElement;
 use Drupal\webform\Plugin\WebformElementManagerInterface;
 use Drupal\webform\Utility\WebformArrayHelper;
 use Drupal\webform\Utility\WebformElementHelper;
@@ -38,9 +39,6 @@ class WebformSubmissionConditionsValidator implements WebformSubmissionCondition
     'open' => '!collapsed',
     'closed' => 'collapsed',
     'readwrite' => '!readonly',
-    // Below states are never used by the #states API.
-    // 'untouched' => '!touched',
-    // 'irrelevant' => '!relevant',
   ];
 
   /**
@@ -61,24 +59,24 @@ public function __construct(WebformElementManagerInterface $element_manager) {
   }
 
   /****************************************************************************/
-  // Webform submission form methods.
+  // Build form methods.
   /****************************************************************************/
 
   /**
    * {@inheritdoc}
-   *
-   * @see \Drupal\webform\WebformSubmissionForm::buildForm
    */
   public function buildForm(array &$form, FormStateInterface $form_state) {
     /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
     $webform_submission = $form_state->getFormObject()->getEntity();
 
-    // Get build and get visible form elements.
-    $elements = &$this->getBuildElements($form);
+    // Get build/visible form elements.
+    $visible_elements = &$this->getBuildElements($form);
 
     // Loop through visible elements with #states.
-    foreach ($elements as &$element) {
-      $states = WebformElementHelper::getStates($element);
+    foreach ($visible_elements as &$element) {
+      $states =& WebformElementHelper::getStates($element);
+      // Store original #states in #_webform_states.
+      $element['#_webform_states'] = $states;
       foreach ($states as $original_state => $conditions) {
         if (!is_array($conditions)) {
           continue;
@@ -87,18 +85,36 @@ public function buildForm(array &$form, FormStateInterface $form_state) {
         // Process state/negate.
         list($state, $negate) = $this->processState($original_state);
 
-        // @todo Track an element's states.
         // If hide/show we need to make sure that validation is not triggered.
         if (strpos($state, 'visible') === 0) {
           $element['#after_build'][] = [get_class($this), 'elementAfterBuild'];
         }
 
-        // Skip if conditions targets are visible.
-        if ($this->isConditionsTargetsVisible($conditions, $elements)) {
+        $targets = $this->getConditionTargetsVisiblity($conditions, $visible_elements);
+
+        // Determine if targets are visible or cross page.
+        $all_targets_visible = (array_sum($targets) === count($targets));
+        $has_cross_page_targets = (!$all_targets_visible && array_sum($targets));
+
+        // Skip if evaluating conditions when all targets are visible.
+        if ($all_targets_visible) {
+          continue;
+        }
+
+        // Replace hidden cross page targets with hidden inputs.
+        if ($has_cross_page_targets) {
+          $cross_page_targets = array_filter(
+            $targets,
+            function ($visible) {
+              return $visible === FALSE;
+            }
+          );
+          $states[$original_state] = $this->replaceCrossPageTargets($conditions, $webform_submission, $cross_page_targets, $form);
           continue;
         }
 
         $result = $this->validateConditions($conditions, $webform_submission);
+
         // Skip invalid conditions.
         if ($result === NULL) {
           continue;
@@ -142,77 +158,87 @@ public function buildForm(array &$form, FormStateInterface $form_state) {
     }
   }
 
-  /****************************************************************************/
-  // Element hide/show validation methods.
-  /****************************************************************************/
-
   /**
-   * Webform element #after_build callback: Wrap #element_validate so that we suppress element validation errors.
+   * Replace hidden cross page targets with hidden inputs.
+   *
+   * @param array $conditions
+   *   An element's conditions.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   * @param array $targets
+   *   An array of hidden target selectors.
+   * @param array $form
+   *   A form.
+   *
+   * @return array
+   *   The conditions with cross page targets replaced with hidden inputs.
    */
-  public static function elementAfterBuild(array $element, FormStateInterface $form_state) {
-    return WebformElementHelper::setElementValidate($element, [get_called_class(), 'elementValidate']);
-  }
+  public function replaceCrossPageTargets(array $conditions, WebformSubmissionInterface $webform_submission, array $targets, array &$form) {
+    // Cache random cross page values.
+    static $cross_page_values = [];
 
-  /**
-   * Webform conditional #element_validate callback: Execute #element_validate and suppress errors.
-   */
-  public static function elementValidate(array &$element, FormStateInterface $form_state) {
-    // Element validation is trigger sequentially.
-    // Triggers must be validated before dependants.
-    //
-    // Build webform submission with validated and processed data.
-    // Webform submission must be rebuilt every time since the
-    // $element and $form_state values can be changed by validation callbacks.
-    /** @var \Drupal\webform\WebformSubmissionForm $form_object */
-    $form_object = $form_state->getFormObject();
-    $complete_form = &$form_state->getCompleteForm();
-    $webform_submission = $form_object->buildEntity($complete_form, $form_state);
+    $cross_page_conditions = [];
+    foreach ($conditions as $index => $value) {
+      if (is_int($index) && is_array($value) && WebformArrayHelper::isSequential($value)) {
+        $cross_page_conditions[$index] = $this->replaceCrossPageTargets($conditions, $webform_submission, $targets, $form);
+      }
+      else {
+        $cross_page_conditions[$index] = $value;
 
-    /** @var \Drupal\webform\WebformSubmissionConditionsValidatorInterface $conditions_validator */
-    $conditions_validator = \Drupal::service('webform_submission.conditions_validator');
-    if ($conditions_validator->isElementVisible($element, $webform_submission)) {
-      WebformElementHelper::triggerElementValidate($element, $form_state);
-    }
-    else {
-      WebformElementHelper::suppressElementValidate($element, $form_state);
-    }
-  }
+        if (is_string($value) && in_array($value, ['and', 'or', 'xor'])) {
+          continue;
+        }
+        elseif (is_int($index)) {
+          $selector = key($value);
+          $condition = $value[$selector];
+        }
+        else {
+          $selector = $index;
+          $condition = $value;
+        }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function isElementVisible(array $element, WebformSubmissionInterface $webform_submission) {
-    $states = WebformElementHelper::getStates($element);
+        if (!isset($targets[$selector])) {
+          continue;
+        }
 
-    $visible = TRUE;
-    foreach ($states as $state => $conditions) {
-      if (!is_array($conditions)) {
-        continue;
-      }
+        $condition_result = $this->validateCondition($selector, $condition, $webform_submission);
+        if ($condition_result === NULL) {
+          continue;
+        }
 
-      // Process state/negate.
-      list($state, $negate) = $this->processState($state);
+        $target_trigger = $condition_result ? 'value' : '!value';
+        $target_name = 'webform_states_' . md5($selector);
+        $target_selector = ':input[name="' . $target_name . '"]';
 
-      $result = $this->validateConditions($conditions, $webform_submission);
-      // Skip invalid conditions.
-      if ($result === NULL) {
-        continue;
-      }
+        // IMPORTANT:
+        // Using a random value to make sure users can't determine a hidden
+        // or computed element's value/result.
+        if (!isset($cross_page_values[$target_name])) {
+          $cross_page_values[$target_name] = rand();
+        }
+        $target_value = $cross_page_values[$target_name];
 
-      // Negate the result.
-      $result = ($negate) ? !$result : $result;
+        if (is_int($index)) {
+          unset($cross_page_conditions[$index][$selector]);
+          $cross_page_conditions[$index][$target_selector] = [$target_trigger => $target_value];
+        }
+        else {
+          unset($cross_page_conditions[$selector]);
+          $cross_page_conditions[$target_selector] = [$target_trigger => $target_value];
+        }
 
-      // Apply result to element state.
-      if (strpos($state, 'visible') === 0 && $result === FALSE) {
-        $visible = FALSE;
+        // Append cross page element's result as a hidden input.
+        $form[$target_name] = [
+          '#type' => 'hidden',
+          '#value' => $target_value,
+        ];
       }
     }
-
-    return $visible;
+    return $cross_page_conditions;
   }
 
   /****************************************************************************/
-  // Validation methods.
+  // Validate form methods.
   /****************************************************************************/
 
   /**
@@ -234,7 +260,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
    */
   protected function validateFormRecursive(array $form, FormStateInterface $form_state) {
     foreach ($form as $key => $element) {
-      if (!Element::child($key) || !is_array($element)) {
+      if (!WebformElementHelper::isElement($element, $key)) {
         continue;
       }
 
@@ -296,6 +322,140 @@ protected function validateFormElement(array $element, FormStateInterface $form_
     }
   }
 
+  /****************************************************************************/
+  // Submit form methods.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
+    $webform_submission = $form_state->getFormObject()->getEntity();
+
+    // Get submission data.
+    $data = $webform_submission->getData();
+
+    // Recursive through the form and unset unset submission data for
+    // form elements that are hidden.
+    $this->submitFormRecursive($form, $webform_submission, $data);
+
+    // Set submission data.
+    $webform_submission->setData($data);
+  }
+
+  /**
+   * Recursively unset submission data for form elements that are hidden.
+   *
+   * @param array $elements
+   *   An array of form elements.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   * @param array $data
+   *   A webform submission's data.
+   * @param bool $visible
+   *   Flag that determine if the currrent form elements are visible.
+   */
+  protected function submitFormRecursive(array $elements, WebformSubmissionInterface $webform_submission, array &$data, $visible = TRUE) {
+    foreach ($elements as $key => &$element) {
+      if (!WebformElementHelper::isElement($element, $key)) {
+        continue;
+      }
+
+      // Skip if element's #states_clear is FALSE.
+      if (isset($element['#states_clear']) && $element['#states_clear'] === FALSE) {
+        continue;
+      }
+
+      if (isset($element['#_webform_access']) && $element['#_webform_access'] === FALSE) {
+        continue;
+      }
+
+      // Determine if the element is visible.
+      $element_visible = ($visible && $this->isElementVisible($element, $webform_submission)) ? TRUE : FALSE;
+
+      // Set data to empty array or string for any webform element that is hidden.
+      if (!$element_visible && !empty($element['#webform_key']) && isset($data[$key])) {
+        $data[$key] = (is_array($data[$key])) ? [] : '';
+      }
+
+      $this->submitFormRecursive($element, $webform_submission, $data, $element_visible);
+    }
+  }
+
+  /****************************************************************************/
+  // Element hide/show validation methods.
+  /****************************************************************************/
+
+  /**
+   * Webform element #after_build callback: Wrap #element_validate so that we suppress element validation errors.
+   */
+  public static function elementAfterBuild(array $element, FormStateInterface $form_state) {
+    return WebformElementHelper::setElementValidate($element, [get_called_class(), 'elementValidate']);
+  }
+
+  /**
+   * Webform conditional #element_validate callback: Execute #element_validate and suppress errors.
+   */
+  public static function elementValidate(array &$element, FormStateInterface $form_state) {
+    // Element validation is trigger sequentially.
+    // Triggers must be validated before dependants.
+    //
+    // Build webform submission with validated and processed data.
+    // Webform submission must be rebuilt every time since the
+    // $element and $form_state values can be changed by validation callbacks.
+    /** @var \Drupal\webform\WebformSubmissionForm $form_object */
+    $form_object = $form_state->getFormObject();
+    $complete_form = &$form_state->getCompleteForm();
+    $webform_submission = $form_object->buildEntity($complete_form, $form_state);
+
+    /** @var \Drupal\webform\WebformSubmissionConditionsValidatorInterface $conditions_validator */
+    $conditions_validator = \Drupal::service('webform_submission.conditions_validator');
+    if ($conditions_validator->isElementVisible($element, $webform_submission)) {
+      WebformElementHelper::triggerElementValidate($element, $form_state);
+    }
+    else {
+      WebformElementHelper::suppressElementValidate($element, $form_state);
+    }
+  }
+
+  /****************************************************************************/
+  // Element state methods.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isElementVisible(array $element, WebformSubmissionInterface $webform_submission) {
+    $states = WebformElementHelper::getStates($element);
+
+    $visible = TRUE;
+    foreach ($states as $state => $conditions) {
+      if (!is_array($conditions)) {
+        continue;
+      }
+
+      // Process state/negate.
+      list($state, $negate) = $this->processState($state);
+
+      $result = $this->validateConditions($conditions, $webform_submission);
+      // Skip invalid conditions.
+      if ($result === NULL) {
+        continue;
+      }
+
+      // Negate the result.
+      $result = ($negate) ? !$result : $result;
+
+      // Apply result to element state.
+      if (strpos($state, 'visible') === 0 && $result === FALSE) {
+        $visible = FALSE;
+      }
+    }
+
+    return $visible;
+  }
+
   /****************************************************************************/
   // Validate state methods.
   /****************************************************************************/
@@ -327,54 +487,48 @@ public function validateState($state, array $conditions, WebformSubmissionInterf
    * {@inheritdoc}
    */
   public function validateConditions(array $conditions, WebformSubmissionInterface $webform_submission) {
-    $condition_logic = 'and';
+    // Determine condition logic.
+    // @see Drupal.states.Dependent.verifyConstraints
+    if (WebformArrayHelper::isSequential($conditions)) {
+      $condition_logic = (in_array('xor', $conditions)) ? 'xor' : 'or';
+    }
+    else {
+      $condition_logic = 'and';
+    }
+
     $condition_results = [];
 
     foreach ($conditions as $index => $value) {
+      // Skip and, or, and xor.
       if (is_string($value) && in_array($value, ['and', 'or', 'xor'])) {
-        $condition_logic = $value;
-        // If OR conditional logic operatator, check current condition
-        // results.
-        if ($condition_logic === 'or' && array_sum($condition_results)) {
-          return TRUE;
-        }
         continue;
       }
-      elseif (is_int($index)) {
-        $selector = key($value);
-        $condition = $value[$selector];
-      }
-      else {
-        $selector = $index;
-        $condition = $value;
-      }
-
-      // Ignore invalid selector and return NULL.
-      $input_name = static::getSelectorInputName($selector);
-      if (!$input_name) {
-        return NULL;
-      }
-
-      $element_key = static::getInputNameAsArray($input_name, 0);
-
-      // Ignore missing dependee element and return NULL.
-      $element = $webform_submission->getWebform()->getElement($element_key);
-      if (!$element) {
-        return NULL;
-      }
 
-      // Issue #1149078: States API doesn't work with multiple select fields.
-      // @see https://www.drupal.org/project/drupal/issues/1149078
-      if (WebformArrayHelper::isSequential($condition)) {
-        $sub_condition_results = [];
-        foreach ($condition as $sub_condition) {
-          $sub_condition_results[] = $this->checkCondition($element, $selector, $sub_condition, $webform_submission);
+      if (is_int($index) && is_array($value)) {
+        // Validate nested conditions.
+        // NOTE: Nested conditions is not supported via the UI.
+        $nested_result = $this->validateConditions($value, $webform_submission);
+        if ($nested_result === NULL) {
+          return NULL;
         }
-        // Evalute sub-conditions using the 'OR' operator.
-        $condition_results[$selector] = (boolean) array_sum($sub_condition_results);
+        $condition_results[] = $nested_result;
       }
       else {
-        $condition_results[$selector] = $this->checkCondition($element, $selector, $condition, $webform_submission);
+        if (is_int($index)) {
+          $selector = key($value);
+          $condition = $value[$selector];
+        }
+        else {
+          $selector = $index;
+          $condition = $value;
+        }
+
+        $condition_result = $this->validateCondition($selector, $condition, $webform_submission);
+        if ($condition_result === NULL) {
+          return NULL;
+        }
+
+        $condition_results[] = $this->validateCondition($selector, $condition, $webform_submission);
       }
     }
 
@@ -397,6 +551,59 @@ public function validateConditions(array $conditions, WebformSubmissionInterface
     }
   }
 
+  /**
+   * Validate #state condition.
+   *
+   * @param string $selector
+   *   The #state condition selector.
+   * @param array $condition
+   *   A condition.
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
+   *
+   * @return bool|null
+   *   TRUE if the condition validates. NULL if condition can't be processed.
+   *   NULL is returned when there is invalid selector and missing element
+   *   in the conditions.
+   */
+  protected function validateCondition($selector, array $condition, WebformSubmissionInterface $webform_submission) {
+    // Ignore invalid selector and return NULL.
+    $input_name = static::getSelectorInputName($selector);
+    if (!$input_name) {
+      return NULL;
+    }
+
+    $element_key = static::getInputNameAsArray($input_name, 0);
+    $element = $webform_submission->getWebform()->getElement($element_key);
+
+    // If no element is found try checking file uploads which use
+    // :input[name="files[ELEMENT_KEY].
+    // @see \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::getElementSelectorOptions
+    if (!$element && strpos($selector, ':input[name="files[') === 0) {
+      $element_key = static::getInputNameAsArray($input_name, 1);
+      $element = $webform_submission->getWebform()->getElement($element_key);
+    }
+
+    // Ignore missing dependee element and return NULL.
+    if (!$element) {
+      return NULL;
+    }
+
+    // Issue #1149078: States API doesn't work with multiple select fields.
+    // @see https://www.drupal.org/project/drupal/issues/1149078
+    if (WebformArrayHelper::isSequential($condition)) {
+      $sub_condition_results = [];
+      foreach ($condition as $sub_condition) {
+        $sub_condition_results[] = $this->checkCondition($element, $selector, $sub_condition, $webform_submission);
+      }
+      // Evaluate sub-conditions using the 'OR' operator.
+      return (boolean) array_sum($sub_condition_results);
+    }
+    else {
+      return $this->checkCondition($element, $selector, $condition, $webform_submission);
+    }
+  }
+
   /**
    * Check a condition.
    *
@@ -417,6 +624,12 @@ protected function checkCondition(array $element, $selector, array $condition, W
     $trigger_value = $condition[$trigger_state];
 
     $element_plugin = $this->elementManager->getElementInstance($element);
+
+    // Ignored conditions for generic webform elements.
+    if ($element_plugin instanceof WebformElement) {
+      return TRUE;
+    }
+
     $element_value = $element_plugin->getElementSelectorInputValue($selector, $trigger_state, $element, $webform_submission);
 
     // Process trigger sub state used for custom #states API validation.
@@ -437,7 +650,8 @@ protected function checkCondition(array $element, $selector, array $condition, W
     // @see \Drupal\webform\Element\WebformElementStates::processWebformStates
     switch ($trigger_state) {
       case 'empty':
-        $result = (empty($element_value) === (boolean) $trigger_value);
+        $empty = (empty($element_value) && $element_value !== '0');
+        $result = ($empty === (boolean) $trigger_value);
         break;
 
       case 'checked':
@@ -456,16 +670,19 @@ protected function checkCondition(array $element, $selector, array $condition, W
         break;
 
       case 'pattern':
-        // @see \Drupal\Core\Render\Element\FormElement::validatePattern
-        $result = preg_match('{' . $trigger_value . '}', $element_value);
+        // PHP: Convert JavaScript-escaped Unicode characters to PCRE
+        // escape sequence format.
+        // @see \Drupal\webform\Plugin\WebformElement\TextBase::validatePattern
+        $pcre_pattern = preg_replace('/\\\\u([a-fA-F0-9]{4})/', '\\x{\\1}', $trigger_value);
+        $result = preg_match('{' . $pcre_pattern . '}u', $element_value);
         break;
 
       case 'less':
-        $result = ($element_value !== '' && $trigger_value > $element_value);
+        $result = ($element_value !== '' && floatval($trigger_value) > floatval($element_value));
         break;
 
       case 'greater':
-        $result = ($element_value !== '' && $trigger_value < $element_value);
+        $result = ($element_value !== '' && floatval($trigger_value) < floatval($element_value));
         break;
 
       default:
@@ -536,7 +753,7 @@ protected function &getBuildElements(array &$form) {
    */
   protected function getBuildElementsRecusive(array &$elements, array &$form, array $parent_states = []) {
     foreach ($form as $key => &$element) {
-      if (Element::property($key) || !is_array($element)) {
+      if (!WebformElementHelper::isElement($element, $key)) {
         continue;
       }
 
@@ -596,6 +813,13 @@ protected function getBuildElementsRecusive(array &$elements, array &$form, arra
         }
       }
 
+      // Store original #access in #_webform_access for all elements.
+      // @see \Drupal\webform\WebformSubmissionConditionsValidator::submitFormRecursive
+      if (isset($element['#access'])) {
+        $element['#_webform_access'] = $element['#access'];
+      }
+
+      // Skip if element is not visible.
       if (isset($element['#access']) && $element['#access'] === FALSE) {
         continue;
       }
@@ -603,23 +827,94 @@ protected function getBuildElementsRecusive(array &$elements, array &$form, arra
       $elements[$key] = &$element;
 
       $this->getBuildElementsRecusive($elements, $element, $subelement_states);
+
+      $element_plugin = $this->elementManager->getElementInstance($element);
+      if ($element_plugin instanceof WebformCompositeBase && !$element_plugin->hasMultipleValues($element)) {
+        // Handle composite with single item.
+        if ($subelement_states) {
+          $composite_elements = $element_plugin->getCompositeElements();
+          foreach ($composite_elements as $composite_key => $composite_element) {
+            // Skip if #access is set to FALSE.
+            if (isset($element['#' . $composite_key . '__access']) && $element['#' . $composite_key . '__access'] === FALSE) {
+              continue;
+            }
+            // Move #composite__required to #composite___required which triggers
+            // conditional #_required handling.
+            if (!empty($element['#' . $composite_key . '__required'])) {
+              unset($element['#' . $composite_key . '__required']);
+              $element['#' . $composite_key . '___required'] = TRUE;
+              $element['#' . $composite_key . '__states'] = $subelement_states;
+            }
+          }
+        }
+      }
+      elseif (isset($element['#element']) && isset($element['#webform_composite_elements'])) {
+        // Handle composite with multiple items and custom composite elements.
+        //
+        // For $element['#elements'] ...
+        // @see \Drupal\webform\Plugin\WebformElement\WebformCompositeBase::prepareMultipleWrapper
+        //
+        // For $element['#webform_composite_elements'] ...
+        // @see \Drupal\webform\Plugin\WebformElement\WebformCompositeBase::initializeCompositeElements
+        // @see \Drupal\webform_composite\Plugin\WebformElement\WebformComposite::initializeCompositeElements
+        //
+        // Recurse through a composite's sub elements.
+        $this->getBuildElementsRecusive($elements, $element['#element'], $subelement_states);
+      }
     }
   }
 
   /**
-   * Determine if #states conditions targets are visible.
+   * Get the visibility state for all of conditions targets.
    *
    * @param array $conditions
    *   An associative array containing conditions.
    * @param array $elements
    *   An associative array of visible elements.
    *
-   * @return bool
-   *   TRUE if ALL #states conditions targets are visible.
+   * @return array
+   *   An associative array keyed by target selectors with a boolean state.
    */
-  protected function isConditionsTargetsVisible(array $conditions, array $elements) {
+  protected function getConditionTargetsVisiblity(array $conditions, array $elements) {
+    $targets = [];
+    $this->getConditionTargetsVisiblityRecursive($conditions, $targets);
+    foreach ($targets as $selector) {
+      // Ignore invalid selector and return FALSE.
+      $input_name = static::getSelectorInputName($selector);
+      if (!$input_name) {
+        $targets[$selector] = FALSE;
+        continue;
+      }
+
+      // Check if the input's element is visible.
+      $element_key = static::getInputNameAsArray($input_name, 0);
+      if (!isset($elements[$element_key])) {
+        $targets[$selector] = FALSE;
+        continue;
+      }
+
+      $targets[$selector] = TRUE;
+    }
+    return $targets;
+  }
+
+  /**
+   * Recursively collect a conditions targets.
+   *
+   * @param array $conditions
+   *   An associative array containing conditions.
+   * @param array $targets
+   *   An associative array keyed by target selectors with a boolean state.
+   */
+  protected function getConditionTargetsVisiblityRecursive(array $conditions, array &$targets = []) {
     foreach ($conditions as $index => $value) {
-      if (is_string($value) && in_array($value, ['and', 'or', 'xor'])) {
+      if (is_int($index) && is_array($value) && WebformArrayHelper::isSequential($value)) {
+        // Recurse downward and get nested target element.
+        // NOTE: Nested conditions is not supported via the UI.
+        $this->getConditionTargetsVisiblityRecursive($value, $targets);
+      }
+      elseif (is_string($value) && in_array($value, ['and', 'or', 'xor'])) {
+        // Skip AND, OR, or XOR operators.
         continue;
       }
       elseif (is_int($index)) {
@@ -629,14 +924,7 @@ protected function isConditionsTargetsVisible(array $conditions, array $elements
         $selector = $index;
       }
 
-      // Ignore invalid selector and return FALSE.
-      $input_name = static::getSelectorInputName($selector);
-      if (!$input_name) {
-        return FALSE;
-      }
-
-      $element_key = static::getInputNameAsArray($input_name, 0);
-      return isset($elements[$element_key]) ? TRUE : FALSE;
+      $targets[$selector] = $selector;
     }
   }
 
diff --git a/web/modules/webform/src/WebformSubmissionConditionsValidatorInterface.php b/web/modules/webform/src/WebformSubmissionConditionsValidatorInterface.php
index c1e59bc7d87af4a7d76c32ec2d025fdc2f5efdbc..6c61df1d7bf978f3ac216cfdf33946e6d1a36222 100644
--- a/web/modules/webform/src/WebformSubmissionConditionsValidatorInterface.php
+++ b/web/modules/webform/src/WebformSubmissionConditionsValidatorInterface.php
@@ -16,6 +16,8 @@ interface WebformSubmissionConditionsValidatorInterface {
    *   An associative array containing the structure of the form.
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The current state of the form.
+   *
+   * @see \Drupal\webform\WebformSubmissionForm::buildForm
    */
   public function buildForm(array &$form, FormStateInterface $form_state);
 
@@ -26,9 +28,23 @@ public function buildForm(array &$form, FormStateInterface $form_state);
    *   An associative array containing the structure of the form.
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The current state of the form.
+   *
+   * @see \Drupal\webform\WebformSubmissionForm::validateForm
    */
   public function validateForm(array &$form, FormStateInterface $form_state);
 
+  /**
+   * Submit form #states for visible elements.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @see \Drupal\webform\WebformSubmissionForm::submitForm
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state);
+
   /**
    * Validate state with conditions.
    *
@@ -53,7 +69,9 @@ public function validateState($state, array $conditions, WebformSubmissionInterf
    *   A webform submission.
    *
    * @return bool|null
-   *   TRUE if conditions validate. NULL if conditions can't be processed.
+   *   TRUE if the conditions validate. NULL if the conditions can't be
+   *   processed. NULL is returned when there is an invalid selector or a
+   *   missing element in the conditions.
    *
    * @see drupal_process_states()
    */
diff --git a/web/modules/webform/src/WebformSubmissionExporter.php b/web/modules/webform/src/WebformSubmissionExporter.php
index 195482024982a4e0d04d46838d1e9bd8a08734aa..ee6e41f4ae632b53ede6583af1d9a45ecef210db 100644
--- a/web/modules/webform/src/WebformSubmissionExporter.php
+++ b/web/modules/webform/src/WebformSubmissionExporter.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\webform;
 
+use Drupal\Core\Archiver\ArchiverManager;
 use Drupal\Core\Archiver\ArchiveTar;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Entity\EntityInterface;
@@ -51,6 +52,13 @@ class WebformSubmissionExporter implements WebformSubmissionExporterInterface {
    */
   protected $streamWrapperManager;
 
+  /**
+   * The archiver manager.
+   *
+   * @var \Drupal\Core\Archiver\ArchiverManager
+   */
+  protected $archiverManager;
+
   /**
    * Webform element manager.
    *
@@ -111,16 +119,19 @@ class WebformSubmissionExporter implements WebformSubmissionExporterInterface {
    *   The entity type manager.
    * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
    *   The stream wrapper manager.
+   * @param \Drupal\Core\Archiver\ArchiverManager $archiver_manager
+   *   The archiver manager.
    * @param \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager
    *   The webform element manager.
    * @param \Drupal\webform\Plugin\WebformExporterManagerInterface $exporter_manager
    *   The results exporter manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, FileSystemInterface $file_system, EntityTypeManagerInterface $entity_type_manager, StreamWrapperManagerInterface $stream_wrapper_manager, WebformElementManagerInterface $element_manager, WebformExporterManagerInterface $exporter_manager) {
+  public function __construct(ConfigFactoryInterface $config_factory, FileSystemInterface $file_system, EntityTypeManagerInterface $entity_type_manager, StreamWrapperManagerInterface $stream_wrapper_manager, ArchiverManager $archiver_manager, WebformElementManagerInterface $element_manager, WebformExporterManagerInterface $exporter_manager) {
     $this->configFactory = $config_factory;
     $this->fileSystem = $file_system;
     $this->entityStorage = $entity_type_manager->getStorage('webform_submission');
     $this->streamWrapperManager = $stream_wrapper_manager;
+    $this->archiverManager = $archiver_manager;
     $this->elementManager = $element_manager;
     $this->exporterManager = $exporter_manager;
   }
@@ -264,7 +275,13 @@ public function getDefaultExportOptions() {
       'files' => FALSE,
     ];
 
-    // Append element handler default options.
+    // Append webform exporter default options.
+    $exporter_plugins = $this->exporterManager->getInstances();
+    foreach ($exporter_plugins as $element_type => $element_plugin) {
+      $this->defaultOptions += $element_plugin->defaultConfiguration();
+    }
+
+    // Append webform element default options.
     $element_types = $this->getWebformElementTypes();
     $element_plugins = $this->elementManager->getInstances();
     foreach ($element_plugins as $element_type => $element_plugin) {
@@ -289,6 +306,11 @@ public function buildExportOptionsForm(array &$form, FormStateInterface $form_st
     $exporter_plugins = $this->exporterManager->getInstances($export_options);
     $states_archive = ['invisible' => []];
     $states_options = ['invisible' => []];
+    $states_files = [
+      'invisible' => [
+        [':input[name="download"]' => ['checked' => FALSE]],
+      ],
+    ];
     foreach ($exporter_plugins as $plugin_id => $exporter_plugin) {
       if ($exporter_plugin->isArchive()) {
         if ($states_archive['invisible']) {
@@ -296,6 +318,12 @@ public function buildExportOptionsForm(array &$form, FormStateInterface $form_st
         }
         $states_archive['invisible'][] = [':input[name="exporter"]' => ['value' => $plugin_id]];
       }
+      if (!$exporter_plugin->hasFiles()) {
+        if ($states_archive['invisible']) {
+          $states_files['invisible'][] = 'or';
+        }
+        $states_files['invisible'][] = [':input[name="exporter"]' => ['value' => $plugin_id]];
+      }
       if (!$exporter_plugin->hasOptions()) {
         if ($states_options['invisible']) {
           $states_options['invisible'][] = 'or';
@@ -304,6 +332,17 @@ public function buildExportOptionsForm(array &$form, FormStateInterface $form_st
       }
     }
 
+    $form['#attributes']['data-webform-states-no-clear'] = TRUE;
+
+    // Build the list of exporter descriptions.
+    $exporters = $this->exporterManager->getInstances();
+    $exporter_description = '';
+    foreach ($exporters as $exporter) {
+      $exporter_description .= '<hr/>';
+      $exporter_description .= '<div><strong>' . $exporter->label() . '</strong></div>';
+      $exporter_description .= '<div>' . $exporter->description() . '</div>';
+    }
+
     $form['export']['format'] = [
       '#type' => 'details',
       '#title' => $this->t('Format options'),
@@ -313,6 +352,7 @@ public function buildExportOptionsForm(array &$form, FormStateInterface $form_st
       '#type' => 'select',
       '#title' => $this->t('Export format'),
       '#options' => $this->exporterManager->getOptions(),
+      '#description' => $exporter_description,
       '#default_value' => $export_options['exporter'],
       // Below .js-webform-exporter is used for exporter configuration form
       // #states.
@@ -470,11 +510,7 @@ public function buildExportOptionsForm(array &$form, FormStateInterface $form_st
       '#return_value' => TRUE,
       '#default_value' => ($webform->hasManagedFile()) ? $export_options['files'] : 0,
       '#access' => $webform->hasManagedFile(),
-      '#states' => [
-        'invisible' => [
-          ':input[name="download"]' => ['checked' => FALSE],
-        ],
-      ],
+      '#states' => $states_files,
     ];
 
     $source_entity = $this->getSourceEntity();
@@ -492,14 +528,14 @@ public function buildExportOptionsForm(array &$form, FormStateInterface $form_st
         $form['export']['download']['submitted']['entity_type'] = [
           '#type' => 'select',
           '#title' => $this->t('Entity type'),
-          '#title_display' => 'Invisible',
+          '#title_display' => 'invisible',
           '#options' => ['' => $this->t('All')] + $entity_types,
           '#default_value' => $export_options['entity_type'],
         ];
         $form['export']['download']['submitted']['entity_id'] = [
           '#type' => 'number',
           '#title' => $this->t('Entity id'),
-          '#title_display' => 'Invisible',
+          '#title_display' => 'invisible',
           '#min' => 1,
           '#size' => 10,
           '#default_value' => $export_options['entity_id'],
@@ -556,15 +592,15 @@ public function buildExportOptionsForm(array &$form, FormStateInterface $form_st
         ],
       ];
       $form['export']['download'][$key]['range_start'] = $range_element + [
-          '#title' => $this->t('From'),
-          '#parents' => [$key, 'range_start'],
-          '#default_value' => $export_options['range_start'],
-        ];
+        '#title' => $this->t('From'),
+        '#parents' => [$key, 'range_start'],
+        '#default_value' => $export_options['range_start'],
+      ];
       $form['export']['download'][$key]['range_end'] = $range_element + [
-          '#title' => $this->t('To'),
-          '#parents' => [$key, 'range_end'],
-          '#default_value' => $export_options['range_end'],
-        ];
+        '#title' => $this->t('To'),
+        '#parents' => [$key, 'range_end'],
+        '#default_value' => $export_options['range_end'],
+      ];
     }
     $form['export']['download']['order'] = [
       '#type' => 'select',
@@ -670,7 +706,7 @@ public function writeRecords(array $webform_submissions) {
     $is_archive = ($this->isArchive() && $export_options['files']);
     $files_directories = [];
     if ($is_archive) {
-      $archiver = new ArchiveTar($this->getArchiveFilePath(), 'gz');
+      $archiver = $this->getArchiveTar();
       $stream_wrappers = array_keys($this->streamWrapperManager->getNames(StreamWrapperInterface::WRITE_VISIBLE));
       foreach ($stream_wrappers as $stream_wrapper) {
         $files_directory = $this->fileSystem->realpath($stream_wrapper . '://webform/' . $webform->id());
@@ -710,9 +746,7 @@ public function writeFooter() {
   public function writeExportToArchive() {
     $export_file_path = $this->getExportFilePath();
     if (file_exists($export_file_path)) {
-      $archive_file_path = $this->getArchiveFilePath();
-
-      $archiver = new ArchiveTar($archive_file_path, 'gz');
+      $archiver = $this->getArchiveTar();
       $archiver->addModify($export_file_path, $this->getBaseFileName(), $this->getFileTempDirectory());
 
       @unlink($export_file_path);
@@ -793,6 +827,7 @@ public function getQuery() {
     if ($export_options['range_type'] == 'latest' && $export_options['range_latest']) {
       // Clone the query and use it to get latest sid starting sid.
       $latest_query = clone $query;
+      $latest_query->sort('created', 'DESC');
       $latest_query->sort('sid', 'DESC');
       $latest_query->range(0, (int) $export_options['range_latest']);
       if ($latest_query_entity_ids = $latest_query->execute()) {
@@ -800,10 +835,16 @@ public function getQuery() {
       }
     }
     else {
-      // Sort by sid in ASC or DESC order.
+      // Sort by created and sid in ASC or DESC order.
+      $query->sort('created', isset($export_options['order']) ? $export_options['order'] : 'ASC');
       $query->sort('sid', isset($export_options['order']) ? $export_options['order'] : 'ASC');
     }
 
+    // Do not check access to submission since the exporter UI and Drush
+    // already have access checking.
+    // @see webform_query_webform_submission_access_alter()
+    $query->accessCheck(FALSE);
+
     return $query;
   }
 
@@ -866,7 +907,7 @@ public function requiresBatch() {
    * {@inheritdoc}
    */
   public function getFileTempDirectory() {
-    return file_directory_temp();
+    return $this->configFactory->get('webform.settings')->get('export.temp_directory') ?: file_directory_temp();
   }
 
   /**
@@ -931,4 +972,26 @@ public function isBatch() {
     return ($this->isArchive() || ($this->getTotal() >= $this->getBatchLimit()));
   }
 
+  /**
+   * Construct an instance of archive tar implementation.
+   *
+   * @return \Drupal\Core\Archiver\ArchiveTar
+   *   Archive tar implementation object.
+   */
+  protected function getArchiveTar() {
+    $archive_tar = $this->archiverManager->getInstance([
+      'filepath' => $this->getArchiveFilePath(),
+    ]);
+
+    $archive_tar = $archive_tar->getArchive();
+
+    if ($archive_tar instanceof ArchiveTar) {
+      // Make it gzip compress.
+      $archive_tar->_compress = TRUE;
+      $archive_tar->_compress_type = 'gz';
+    }
+
+    return $archive_tar;
+  }
+
 }
diff --git a/web/modules/webform/src/WebformSubmissionExporterInterface.php b/web/modules/webform/src/WebformSubmissionExporterInterface.php
index c45980f4367140f934bca37b87d11060c5859d05..4e54fd76431621817daa9caf8ed18a03b97ea5c6 100644
--- a/web/modules/webform/src/WebformSubmissionExporterInterface.php
+++ b/web/modules/webform/src/WebformSubmissionExporterInterface.php
@@ -113,7 +113,7 @@ public function buildExportOptionsForm(array &$form, FormStateInterface $form_st
   /**
    * Get the values from the webform's user input or webform state values.
    *
-   * @paran array $input
+   * @param array $values
    *   An associative array of user input or webform state values.
    *
    * @return array
diff --git a/web/modules/webform/src/WebformSubmissionForm.php b/web/modules/webform/src/WebformSubmissionForm.php
index f4f8ef6118b9e436e082ad3468fa62eefdf3abcc..4f79d946db3b821dbd46d1238b7824ea3703f5b1 100644
--- a/web/modules/webform/src/WebformSubmissionForm.php
+++ b/web/modules/webform/src/WebformSubmissionForm.php
@@ -3,6 +3,7 @@
 namespace Drupal\webform;
 
 use Drupal\Component\Render\PlainTextOutput;
+use Drupal\Component\Utility\Bytes;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
@@ -11,6 +12,7 @@
 use Drupal\Core\Entity\ContentEntityForm;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Path\AliasManagerInterface;
 use Drupal\Core\Path\PathValidatorInterface;
 use Drupal\Core\Render\Element;
@@ -18,14 +20,18 @@
 use Drupal\Core\Routing\TrustedRedirectResponse;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Url;
-use Drupal\webform\WebformSubmissionInterface;
 use Drupal\webform\Entity\WebformSubmission;
 use Drupal\webform\Form\WebformDialogFormTrait;
 use Drupal\webform\Plugin\WebformElement\Hidden;
 use Drupal\webform\Plugin\WebformElementManagerInterface;
 use Drupal\webform\Plugin\WebformHandlerInterface;
+use Drupal\webform\Plugin\WebformSourceEntityManager;
 use Drupal\webform\Utility\WebformArrayHelper;
+use Drupal\webform\Utility\WebformDialogHelper;
+use Drupal\webform\Utility\WebformElementHelper;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Drupal\Core\Config\ConfigFactoryInterface;
 
 /**
  * Provides a webform to collect and edit submissions.
@@ -34,6 +40,13 @@ class WebformSubmissionForm extends ContentEntityForm {
 
   use WebformDialogFormTrait;
 
+  /**
+   * The configuration object factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
   /**
    * The renderer service.
    *
@@ -62,13 +75,6 @@ class WebformSubmissionForm extends ContentEntityForm {
    */
   protected $elementManager;
 
-  /**
-   * The webform submission storage.
-   *
-   * @var \Drupal\webform\WebformSubmissionStorageInterface
-   */
-  protected $storage;
-
   /**
    * Webform request handler.
    *
@@ -100,7 +106,7 @@ class WebformSubmissionForm extends ContentEntityForm {
   /**
    * The webform submission conditions (#states) validator.
    *
-   * @var \Drupal\webform\WebformSubmissionConditionsValidator
+   * @var \Drupal\webform\WebformSubmissionConditionsValidatorInterface
    */
   protected $conditionsValidator;
 
@@ -118,13 +124,6 @@ class WebformSubmissionForm extends ContentEntityForm {
    */
   protected $generate;
 
-  /**
-   * The webform settings.
-   *
-   * @var array
-   */
-  protected $settings;
-
   /**
    * The webform submission.
    *
@@ -139,11 +138,20 @@ class WebformSubmissionForm extends ContentEntityForm {
    */
   protected $sourceEntity;
 
+  /**
+   * States API prefix.
+   *
+   * @var string
+   */
+  protected $statesPrefix;
+
   /**
    * Constructs a WebformSubmissionForm object.
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
    *   The entity manager.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The factory for configuration objects.
    * @param \Drupal\Core\Render\RendererInterface $renderer
    *   The renderer service.
    * @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
@@ -167,14 +175,28 @@ class WebformSubmissionForm extends ContentEntityForm {
    * @param \Drupal\webform\WebformSubmissionGenerateInterface $submission_generate
    *   The webform submission generation service.
    */
-  public function __construct(EntityManagerInterface $entity_manager, RendererInterface $renderer, AliasManagerInterface $alias_manager, PathValidatorInterface $path_validator, WebformRequestInterface $request_handler, WebformElementManagerInterface $element_manager, WebformThirdPartySettingsManagerInterface $third_party_settings_manager, WebformMessageManagerInterface $message_manager, WebformTokenManagerInterface $token_manager, WebformSubmissionConditionsValidator $conditions_validator, WebformEntityReferenceManagerInterface $webform_entity_reference_manager, WebformSubmissionGenerateInterface $submission_generate) {
+  public function __construct(
+    EntityManagerInterface $entity_manager,
+    ConfigFactoryInterface $config_factory,
+    RendererInterface $renderer,
+    AliasManagerInterface $alias_manager,
+    PathValidatorInterface $path_validator,
+    WebformRequestInterface $request_handler,
+    WebformElementManagerInterface $element_manager,
+    WebformThirdPartySettingsManagerInterface $third_party_settings_manager,
+    WebformMessageManagerInterface $message_manager,
+    WebformTokenManagerInterface $token_manager,
+    WebformSubmissionConditionsValidator $conditions_validator,
+    WebformEntityReferenceManagerInterface $webform_entity_reference_manager,
+    WebformSubmissionGenerateInterface $submission_generate
+  ) {
     parent::__construct($entity_manager);
+    $this->configFactory = $config_factory;
     $this->renderer = $renderer;
     $this->requestHandler = $request_handler;
     $this->aliasManager = $alias_manager;
     $this->pathValidator = $path_validator;
     $this->elementManager = $element_manager;
-    $this->storage = $this->entityManager->getStorage('webform_submission');
     $this->thirdPartySettingsManager = $third_party_settings_manager;
     $this->messageManager = $message_manager;
     $this->tokenManager = $token_manager;
@@ -190,6 +212,7 @@ public function __construct(EntityManagerInterface $entity_manager, RendererInte
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('entity.manager'),
+      $container->get('config.factory'),
       $container->get('renderer'),
       $container->get('path.alias_manager'),
       $container->get('path.validator'),
@@ -201,7 +224,6 @@ public static function create(ContainerInterface $container) {
       $container->get('webform_submission.conditions_validator'),
       $container->get('webform.entity_reference_manager'),
       $container->get('webform_submission.generate')
-
     );
   }
 
@@ -238,7 +260,6 @@ public function getFormId() {
    * @see \Drupal\Core\Entity\EntityFormBuilder::getForm
    */
   public function setEntity(EntityInterface $entity) {
-
     /** @var \Drupal\webform\WebformSubmissionInterface $entity */
     $webform = $entity->getWebform();
 
@@ -254,33 +275,46 @@ public function setEntity(EntityInterface $entity) {
 
     // Get the source entity and allow webform submission to be used as a source
     // entity.
-    $this->sourceEntity = $this->requestHandler->getCurrentSourceEntity(['webform']);
-    if ($this->sourceEntity === $entity) {
-      $this->sourceEntity = $this->requestHandler->getCurrentSourceEntity(['webform', 'webform_submission']);
+    $source_entity = $entity->getSourceEntity() ?: $this->requestHandler->getCurrentSourceEntity(['webform']);
+    if ($source_entity === $entity) {
+      $source_entity = $this->requestHandler->getCurrentSourceEntity(['webform', 'webform_submission']);
+    }
+    // Handle paragraph sourc entity.
+    if ($source_entity && $source_entity->getEntityTypeId() === 'paragraph') {
+      // Default data supports paragraph source entity tokens.
+      // @see \Drupal\webform\Plugin\Field\FieldFormatter\WebformEntityReferenceEntityFormatter::viewElements
+      $data = $entity->getData();
+      // Disable :clear suffix to prevent webform tokens from being removed.
+      $data = $this->tokenManager->replace($data, $source_entity, [], ['suffixes' => ['clear' => FALSE]]);
+      $entity->setData($data);
+
+      $source_entity = WebformSourceEntityManager::getMainSourceEntity($source_entity);
     }
+    // Set source entity.
+    $this->sourceEntity = $source_entity;
 
-    $source_entity = $this->sourceEntity;
+    // Get account.
+    $account = $this->currentUser();
 
     // Load entity from token or saved draft when not editing or testing
     // submission form.
     if (!in_array($this->operation, ['edit', 'edit_all', 'test'])) {
       $token = $this->getRequest()->query->get('token');
-      $webform_submission_token = $this->storage->loadFromToken($token, $webform, $source_entity);
+      $webform_submission_token = $this->getStorage()->loadFromToken($token, $webform, $source_entity);
       if ($webform_submission_token) {
         $entity = $webform_submission_token;
       }
       elseif ($webform->getSetting('draft') != WebformInterface::DRAFT_NONE) {
-        $account = $this->currentUser();
         if ($webform->getSetting('draft_multiple')) {
           // Allow multiple drafts to be restored using token.
           // This allows the webform's public facing URL to be used instead of
           // the admin URL of the webform.
-          $webform_submission_token = $this->storage->loadFromToken($token, $webform, $source_entity, $account);
+          $webform_submission_token = $this->getStorage()->loadFromToken($token, $webform, $source_entity, $account);
           if ($webform_submission_token && $webform_submission_token->isDraft()) {
             $entity = $webform_submission_token;
           }
         }
-        elseif ($webform_submission_draft = $this->storage->loadDraft($webform, $source_entity, $account)) {
+        elseif ($webform_submission_draft = $this->getStorage()->loadDraft($webform, $source_entity, $account)) {
           // Else load the most recent draft.
           $entity = $webform_submission_draft;
         }
@@ -290,17 +324,46 @@ public function setEntity(EntityInterface $entity) {
     // Set entity before calling get last submission.
     $this->entity = $entity;
 
-    // Autofill with previous submission.
-    if ($this->operation === 'add' && $entity->isNew() && $webform->getSetting('autofill')) {
-      if ($last_submission = $this->getLastSubmission()) {
-        $excluded_elements = $webform->getSetting('autofill_excluded_elements') ?: [];
-        $last_submission_data = array_diff_key($last_submission->getData(), $excluded_elements);
-        $entity->setData($last_submission_data + $entity->getData());
+    if ($entity->isNew()) {
+      if ($webform->getSetting('limit_total_unique')) {
+        // Get last webform/source entity submission.
+        $last_submission = $this->getStorage()->getLastSubmission($webform, $source_entity, NULL, ['in_draft' => FALSE]);
+        if ($last_submission) {
+          $entity = $last_submission;
+          $this->operation = 'edit';
+        }
+      }
+      elseif ($webform->getSetting('limit_user_unique')) {
+        // Require user to be authenticated to access a unique submission.
+        if (!$account->isAuthenticated()) {
+          throw new AccessDeniedHttpException();
+        }
+
+        // Get last user submission.
+        $last_submission = $this->getStorage()->getLastSubmission($webform, $source_entity, $account, ['in_draft' => FALSE]);
+        if ($last_submission) {
+          $entity = $last_submission;
+          $this->operation = 'edit';
+        }
+      }
+    }
+
+    if ($this->operation === 'add' && $entity->isNew()) {
+      if ($webform->getSetting('autofill')) {
+        // Autofill with previous submission.
+        if ($last_submission = $this->getLastSubmission()) {
+          $excluded_elements = $webform->getSetting('autofill_excluded_elements') ?: [];
+          $last_submission_data = array_diff_key($last_submission->getData(), $excluded_elements);
+          $entity->setData($last_submission_data + $entity->getData());
+        }
       }
     }
 
-    // Alter webform settings before setting the entity.
-    $entity->getWebform()->invokeHandlers('overrideSettings', $entity);
+    // Override settings.
+    $this->overrideSettings($entity);
+
+    // Set the webform's current operation.
+    $webform->setOperation($this->operation);
 
     return parent::setEntity($entity);
   }
@@ -312,18 +375,42 @@ public function buildEntity(array $form, FormStateInterface $form_state) {
     /** @var \Drupal\webform\WebformSubmissionInterface $entity */
     $entity = parent::buildEntity($form, $form_state);
 
-    // Alter webform settings before setting the entity.
-    $entity->getWebform()->invokeHandlers('overrideSettings', $entity);
+    // Override settings.
+    $this->overrideSettings($entity);
 
     return $entity;
   }
 
   /**
-   * {@inheritdoc}
+   * Override webform settings for the webform submission.
+   *
+   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
+   *   A webform submission.
    */
-  protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
-    parent::copyFormValuesToEntity($entity, $form, $form_state);
+  protected function overrideSettings(WebformSubmissionInterface $webform_submission) {
+    $webform = $webform_submission->getWebform();
+
+    // Invoke override settings which resets the webform settings.
+    $webform->invokeHandlers('overrideSettings', $webform_submission);
+
+    // Look for ?_webform_dialog=1 which enables Ajax support when this form is
+    // opened in dialog.
+    // @see webform.dialog.js
+    //
+    // Must be called after WebformHandler::overrideSettings which resets all
+    // overridden settings.
+    // @see \Drupal\webform\Entity\Webform::invokeHandlers
+    if ($this->getRequest()->query->get('_webform_dialog') && !$webform->getSetting('ajax')) {
+      $webform->setSettingOverride('ajax', TRUE);
+    }
+  }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
+    // NOTE: We are not copying form values to the entity because
+    // webform element keys can override webform submission properties.
     /* @var $webform_submission \Drupal\webform\WebformSubmissionInterface */
     $webform_submission = $entity;
     $webform = $webform_submission->getWebform();
@@ -341,6 +428,12 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
     if ($current_page = $this->getCurrentPage($form, $form_state)) {
       $entity->setCurrentPage($current_page);
     }
+
+    // Set in draft.
+    $in_draft = $form_state->get('in_draft');
+    if ($in_draft !== NULL) {
+      $entity->set('in_draft', $in_draft);
+    }
   }
 
   /**
@@ -375,6 +468,9 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     // Server side #states API validation.
     $this->conditionsValidator->buildForm($form, $form_state);
 
+    // Add Ajax callbacks.
+    $form = $this->buildAjaxForm($form, $form_state);
+
     // Alter webform via webform handler.
     $this->getWebform()->invokeHandlers('alterForm', $form, $form_state, $webform_submission);
 
@@ -382,7 +478,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $form_id = $this->getFormId();
     $this->thirdPartySettingsManager->alter('webform_submission_form', $form, $form_state, $form_id);
 
-    return $this->buildAjaxForm($form, $form_state);
+    return $form;
   }
 
   /**
@@ -418,6 +514,14 @@ public function form(array $form, FormStateInterface $form_state) {
       }
     }
 
+    // Disable the default $form['#theme'] templates.
+    // If the webform's id begins with an underscore the #theme
+    // was automatically being set to 'webform_submission__WEBFORM_ID', this
+    // causes the form to be rendered using the 'webform_submission' template.
+    // @see \Drupal\Core\Form\FormBuilder::prepareForm
+    // @see webform-submission-form.html.twig
+    $form['#theme'] = ['webform_submission_form'];
+
     // Define very specific webform classes, this override the form's
     // default classes.
     // @see \Drupal\Core\Form\FormBuilder::retrieveForm
@@ -437,6 +541,10 @@ public function form(array $form, FormStateInterface $form_state) {
     array_walk($class, ['\Drupal\Component\Utility\Html', 'getClass']);
     $form['#attributes']['class'] = $class;
 
+    // Get last class, which is the most specific, as #states prefix.
+    // @see \Drupal\webform\WebformSubmissionForm::addStatesPrefix
+    $this->statesPrefix = '.' . end($class);
+
     // Check for a custom webform, track it, and return it.
     if ($custom_form = $this->getCustomForm($form, $form_state)) {
       $custom_form['#custom_form'] = TRUE;
@@ -450,12 +558,12 @@ public function form(array $form, FormStateInterface $form_state) {
     // Prepend webform submission data using the default view without the data.
     if (!$webform_submission->isNew() && !$webform_submission->isDraft()) {
       $form['navigation'] = [
-        '#theme' => 'webform_submission_navigation',
+        '#type' => 'webform_submission_navigation',
         '#webform_submission' => $webform_submission,
         '#weight' => -20,
       ];
       $form['information'] = [
-        '#theme' => 'webform_submission_information',
+        '#type' => 'webform_submission_information',
         '#webform_submission' => $webform_submission,
         '#source_entity' => $this->sourceEntity,
         '#weight' => -19,
@@ -483,8 +591,9 @@ public function form(array $form, FormStateInterface $form_state) {
         ],
         '#attributes' => ['class' => ['js-hide', 'webform-confirmation-modal', 'js-webform-confirmation-modal']],
         '#weight' => -1000,
+        '#attached' => ['library' => ['webform/webform.confirmation.modal']],
+        '#element_validate' => ['::removeConfirmationModal'],
       ];
-      $form['#attached']['library'][] = 'webform/webform.confirmation.modal';
     }
 
     /* Data */
@@ -504,16 +613,25 @@ public function form(array $form, FormStateInterface $form_state) {
     // Prepare webform elements.
     $this->prepareElements($elements, $form, $form_state);
 
-    // Add wizard progress tracker to the webform.
-    $current_page = $this->getCurrentPage($form, $form_state);
-    if ($current_page && $this->getWebformSetting('wizard_progress_bar') || $this->getWebformSetting('wizard_progress_pages') || $this->getWebformSetting('wizard_progress_percentage')) {
-      $form['progress'] = [
-        '#theme' => 'webform_progress',
-        '#webform' => $this->getWebform(),
-        '#current_page' => $current_page,
-        '#operation' => $this->operation,
-        '#weight' => -20,
-      ];
+    // Add wizard progress tracker and page links to the webform.
+    $pages = $webform->getPages($this->operation);
+    if ($pages) {
+      $current_page = $this->getCurrentPage($form, $form_state);
+
+      // Add hidden pages submit actions.
+      $form['pages'] = $this->pagesElement($form, $form_state);
+
+      // Add progress tracker.
+      $display_wizard_progress = ($this->getWebformSetting('wizard_progress_bar') || $this->getWebformSetting('wizard_progress_pages') || $this->getWebformSetting('wizard_progress_percentage'));
+      if ($current_page && $display_wizard_progress) {
+        $form['progress'] = [
+          '#theme' => 'webform_progress',
+          '#webform' => $this->getWebform(),
+          '#current_page' => $current_page,
+          '#operation' => $this->operation,
+          '#weight' => -20,
+        ];
+      }
     }
 
     // Required indicator.
@@ -531,7 +649,7 @@ public function form(array $form, FormStateInterface $form_state) {
     // Pages: Set current wizard or preview page.
     $this->displayCurrentPage($form, $form_state);
 
-    /* Webform  */
+    /* Webform */
 
     // Move all $elements properties to the $form.
     $this->setFormPropertiesFromElements($form, $elements);
@@ -564,6 +682,7 @@ public function form(array $form, FormStateInterface $form_state) {
     if ($this->getWebformSetting('form_unsaved')) {
       $form['#attributes']['class'][] = 'js-webform-unsaved';
       $form['#attached']['library'][] = 'webform/webform.form.unsaved';
+      // Set 'data-webform-unsaved' attribute if unsaved wizard.
       $pages = $this->getPages($form, $form_state);
       $current_page = $this->getCurrentPage($form, $form_state);
       if ($current_page && ($current_page != $this->getFirstPage($pages))) {
@@ -588,7 +707,7 @@ public function form(array $form, FormStateInterface $form_state) {
       $form['#attributes']['novalidate'] = 'novalidate';
     }
 
-    // Inline form errors: Add  #disable_inline_form_errors property to form.
+    // Inline form errors: Add #disable_inline_form_errors property to form.
     if ($this->getWebformSetting('form_disable_inline_errors')) {
       $form['#disable_inline_form_errors'] = TRUE;
     }
@@ -643,12 +762,12 @@ protected function getCustomForm(array &$form, FormStateInterface $form_state) {
     // Exit if elements are broken, usually occurs when elements YAML is edited
     // directly in the export config file.
     if (!$webform_submission->getWebform()->getElementsInitialized()) {
-      return $this->getMessageManager()->append($form, WebformMessageManagerInterface::FORM_EXCEPTION, 'warning');
+      return $this->getMessageManager()->append($form, WebformMessageManagerInterface::FORM_EXCEPTION_MESSAGE, 'warning');
     }
 
     // Exit if submission is locked.
     if ($webform_submission->isLocked()) {
-      return $this->getMessageManager()->append($form, WebformMessageManagerInterface::SUBMISSION_LOCKED, 'warning');
+      return $this->getMessageManager()->append($form, WebformMessageManagerInterface::SUBMISSION_LOCKED_MESSAGE, 'warning');
     }
 
     // Check prepopulate source entity required and type.
@@ -675,6 +794,7 @@ protected function getCustomForm(array &$form, FormStateInterface $form_state) {
       // Add hidden back (aka reset) button used by the Ajaxified back to link.
       // NOTE: Below code could be used to add a 'Reset' button to any webform.
       // @see Drupal.behaviors.webformConfirmationBackAjax
+      $form['actions'] = ['#type' => 'actions'];
       $form['actions']['reset'] = [
         '#type' => 'submit',
         '#value' => $this->t('Reset'),
@@ -690,16 +810,16 @@ protected function getCustomForm(array &$form, FormStateInterface $form_state) {
     }
 
     // Don't display webform if it is closed.
-    if ($webform_submission->isNew() && $webform->isClosed()) {
+    if (($webform_submission->isNew() || $webform_submission->isDraft()) && $webform->isClosed()) {
       // If the current user can update any submission just display the closed
       // message and still allow them to create new submissions.
-      if ($webform->isTemplate() && $webform->access('duplicate')) {
+      if ($webform->isTemplate() && $webform->access('duplicate') && !$webform->isArchived()) {
         if (!$this->isDialog()) {
           $this->getMessageManager()->display(WebformMessageManagerInterface::TEMPLATE_PREVIEW, 'warning');
         }
       }
       elseif ($webform->access('submission_update_any')) {
-        $form = $this->getMessageManager()->append($form, WebformMessageManagerInterface::ADMIN_CLOSED, 'info');
+        $form = $this->getMessageManager()->append($form, $webform->isArchived() ? WebformMessageManagerInterface::ADMIN_ARCHIVED : WebformMessageManagerInterface::ADMIN_CLOSED, 'info');
       }
       else {
         if ($webform->isOpening()) {
@@ -730,12 +850,12 @@ protected function getCustomForm(array &$form, FormStateInterface $form_state) {
       }
       else {
         // Display exception message to users.
-        return $this->getMessageManager()->append($form, WebformMessageManagerInterface::FORM_EXCEPTION, 'warning');
+        return $this->getMessageManager()->append($form, WebformMessageManagerInterface::FORM_EXCEPTION_MESSAGE, 'warning');
       }
     }
 
     // Check total limit.
-    if ($this->checkTotalLimit()) {
+    if ($this->checkTotalLimit() && empty($this->getWebformSetting('limit_total_unique'))) {
       $form = $this->getMessageManager()->append($form, WebformMessageManagerInterface::LIMIT_TOTAL_MESSAGE);
       if ($webform->access('submission_update_any')) {
         $form = $this->getMessageManager()->append($form, WebformMessageManagerInterface::ADMIN_CLOSED, 'info');
@@ -746,7 +866,7 @@ protected function getCustomForm(array &$form, FormStateInterface $form_state) {
     }
 
     // Check user limit.
-    if ($this->checkUserLimit()) {
+    if ($this->checkUserLimit() && empty($this->getWebformSetting('limit_user_unique'))) {
       $form = $this->getMessageManager()->append($form, WebformMessageManagerInterface::LIMIT_USER_MESSAGE, 'warning');
       if ($webform->access('submission_update_any')) {
         $form = $this->getMessageManager()->append($form, WebformMessageManagerInterface::ADMIN_CLOSED, 'info');
@@ -774,7 +894,7 @@ protected function displayMessages(array $form, FormStateInterface $form_state)
     $source_entity = $this->getSourceEntity();
 
     // Display test message.
-    if ($this->isGet() && $this->isRoute('webform.test_form')) {
+    if ($this->isGet() && $this->operation === 'test') {
       $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_TEST, 'warning');
 
       // Display devel generate link for webform or source entity.
@@ -787,12 +907,17 @@ protected function displayMessages(array $form, FormStateInterface $form_state)
           ];
         }
         $query['destination'] = $this->requestHandler->getUrl($webform, $source_entity, 'webform.results_submissions')->toString();
+        $offcanvas = WebformDialogHelper::useOffCanvas();
         $build = [
           '#type' => 'link',
           '#title' => $this->t('Generate %title submissions', ['%title' => $webform->label()]),
           '#url' => Url::fromRoute('devel_generate.webform_submission', [], ['query' => $query]),
+          '#attributes' => ($offcanvas) ? WebformDialogHelper::getOffCanvasDialogAttributes(400) : [],
         ];
-        drupal_set_message($this->renderer->renderPlain($build), 'warning');
+        if ($offcanvas) {
+          WebformDialogHelper::attachLibraries($form);
+        }
+        $this->messenger()->addWarning($this->renderer->renderPlain($build));
       }
     }
 
@@ -807,11 +932,11 @@ protected function displayMessages(array $form, FormStateInterface $form_state)
     // Display loaded or saved draft message.
     if ($webform_submission->isDraft()) {
       if ($form_state->get('draft_saved')) {
-        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_DRAFT_SAVED);
+        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_DRAFT_SAVED_MESSAGE);
         $form_state->set('draft_saved', FALSE);
       }
-      elseif ($this->isGet() && !$webform->getSetting('draft_multiple')) {
-        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_DRAFT_LOADED);
+      elseif ($this->isGet() && !$webform->getSetting('draft_multiple') && !$webform->isClosed()) {
+        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_DRAFT_LOADED_MESSAGE);
       }
     }
 
@@ -821,13 +946,13 @@ protected function displayMessages(array $form, FormStateInterface $form_state)
       && $this->getWebformSetting('draft') !== WebformInterface::DRAFT_NONE
       && $this->getWebformSetting('draft_multiple', FALSE)
       && ($this->isRoute('webform.canonical') || $this->isWebformEntityReferenceFromSourceEntity())
-      && ($previous_draft_total = $this->storage->getTotal($webform, $this->sourceEntity, $this->currentUser(), ['in_draft' => TRUE]))
+      && ($previous_draft_total = $this->getStorage()->getTotal($webform, $this->sourceEntity, $this->currentUser(), ['in_draft' => TRUE]))
     ) {
       if ($previous_draft_total > 1) {
         $this->getMessageManager()->display(WebformMessageManagerInterface::DRAFTS_PREVIOUS);
       }
       else {
-        $draft_submission = $this->storage->loadDraft($webform, $this->sourceEntity, $this->currentUser());
+        $draft_submission = $this->getStorage()->loadDraft($webform, $this->sourceEntity, $this->currentUser());
         if (!$draft_submission || $webform_submission->id() != $draft_submission->id()) {
           $this->getMessageManager()->display(WebformMessageManagerInterface::DRAFT_PREVIOUS);
         }
@@ -840,13 +965,16 @@ protected function displayMessages(array $form, FormStateInterface $form_state)
       && $this->getWebformSetting('form_previous_submissions', FALSE)
       && ($this->isRoute('webform.canonical') || $this->isWebformEntityReferenceFromSourceEntity())
       && ($webform->access('submission_view_own') || $this->currentUser()->hasPermission('view own webform submission'))
-      && ($previous_submission_total = $this->storage->getTotal($webform, $this->sourceEntity, $this->currentUser()))
+      && ($previous_submission_total = $this->getStorage()->getTotal($webform, $this->sourceEntity, $this->currentUser()))
     ) {
       if ($previous_submission_total > 1) {
-        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSIONS_PREVIOUS);
+        $this->getMessageManager()->display(WebformMessageManagerInterface::PREVIOUS_SUBMISSIONS);
       }
-      elseif ($webform_submission->id() != $this->getLastSubmission(FALSE)->id()) {
-        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_PREVIOUS);
+      else {
+        $last_submission = $this->getLastSubmission(FALSE);
+        if ($last_submission && $webform_submission->id() !== $last_submission->id()) {
+          $this->getMessageManager()->display(WebformMessageManagerInterface::PREVIOUS_SUBMISSION);
+        }
       }
     }
 
@@ -856,7 +984,7 @@ protected function displayMessages(array $form, FormStateInterface $form_state)
       && $webform_submission->isNew()
       && $webform->getSetting('autofill')
       && $this->getLastSubmission()) {
-      $this->getMessageManager()->display(WebformMessageManagerInterface::AUTOFILL);
+      $this->getMessageManager()->display(WebformMessageManagerInterface::AUTOFILL_MESSAGE);
     }
   }
 
@@ -884,6 +1012,82 @@ public function afterBuild(array $form, FormStateInterface $form_state) {
     return $form;
   }
 
+  /**
+   * Returns the wizard page submit buttons for the current entity form.
+   */
+  protected function pagesElement(array $form, FormStateInterface $form_state) {
+    $pages = $this->getPages($form, $form_state);
+    if (!$pages) {
+      return NULL;
+    }
+
+    $current_page_name = $this->getCurrentPage($form, $form_state);
+    if (!$this->getWebformSetting('wizard_progress_link') && !($this->getWebformSetting('wizard_preview_link') && $current_page_name === 'webform_preview')) {
+      return NULL;
+    }
+
+    $page_indexes = array_flip(array_keys($pages));
+    $current_index = $page_indexes[$current_page_name] - 1;
+
+    // Build dedicated actions element for pages links.
+    $element = [
+      '#type' => 'actions',
+      '#weight' => -20,
+      '#attributes' => [
+        'class' => ['webform-wizard-pages-links', 'js-webform-wizard-pages-links'],
+      ],
+      // Only process the container and prevent .form-actions from being added
+      // which force submit buttons to be rendered in dialogs.
+      // @see \Drupal\Core\Render\Element\Actions
+      // @see Drupal.behaviors.dialog.prepareDialogButtons
+      '#process' => [
+        ['\Drupal\Core\Render\Element\Actions', 'processContainer'],
+      ],
+    ];
+    if ($this->getWebformSetting('wizard_progress_link')) {
+      $element['#attributes']['data-wizard-progress-link'] = 'true';
+    }
+    if ($this->getWebformSetting('wizard_preview_link')) {
+      $element['#attributes']['data-wizard-preview-link'] = 'true';
+    }
+
+    $index = 1;
+    $total = count($pages);
+    foreach ($pages as $page_name => $page) {
+      // Always include submit button for each page but only allows access
+      // to previous and visible pages.
+      //
+      // Developers who want to allow users to jump to any wizard page can
+      // expose these buttons via a form alter hook. Beware that
+      // skipped pages will not be validated.
+      $access = ($page['#access'] && ($page_indexes[$page_name] <= $current_index)) ? TRUE : FALSE;
+      $t_args = [
+        '@label' => $page['#title'],
+        '@start' => $index++,
+        '@end' => $total,
+      ];
+      $element[$page_name] = [
+        '#type' => 'submit',
+        '#value' => $this->t('Edit'),
+        '#page' => $page_name,
+        '#validate' => ['::noValidate'],
+        '#submit' => ['::gotoPage'],
+        '#name' => 'webform_wizard_page-' . $page_name,
+        '#attributes' => [
+          'data-webform-page' => $page_name,
+          'formnovalidate' => 'formnovalidate',
+          'class' => ['webform-wizard-pages-link', 'js-webform-wizard-pages-link'],
+          'title' => $this->t("Edit '@label' (Page @start of @end)", $t_args),
+        ],
+        '#access' => $access,
+      ];
+    }
+
+    $element['#attached']['library'][] = 'webform/webform.wizard.pages';
+
+    return $element;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -983,7 +1187,10 @@ protected function actions(array $form, FormStateInterface $form_state) {
             // @see \Drupal\webform\WebformSubmissionForm::noValidate
             '#validate' => ['::noValidate'],
             '#submit' => ['::previous'],
-            '#attributes' => ['class' => ['webform-button--previous', 'js-webform-novalidate']],
+            '#attributes' => [
+              'formnovalidate' => 'formnovalidate',
+              'class' => ['webform-button--previous'],
+            ],
             '#weight' => 0,
           ];
           if ($track) {
@@ -1006,7 +1213,10 @@ protected function actions(array $form, FormStateInterface $form_state) {
             // @see \Drupal\webform\WebformSubmissionForm::noValidate
             '#validate' => ['::noValidate'],
             '#submit' => ['::previous'],
-            '#attributes' => ['class' => ['webform-button--previous', 'js-webform-novalidate']],
+            '#attributes' => [
+              'formnovalidate' => 'formnovalidate',
+              'class' => ['webform-button--previous'],
+            ],
             '#weight' => 0,
           ];
           if ($track) {
@@ -1052,7 +1262,9 @@ protected function actions(array $form, FormStateInterface $form_state) {
           }
         }
       }
-      $element['#attached']['library'][] = 'webform/webform.form.wizard';
+      if ($track) {
+        $element['#attached']['library'][] = 'webform/webform.wizard.track';
+      }
     }
 
     // Draft.
@@ -1062,7 +1274,10 @@ protected function actions(array $form, FormStateInterface $form_state) {
         '#value' => $this->config('webform.settings')->get('settings.default_draft_button_label'),
         '#validate' => ['::draft'],
         '#submit' => ['::submitForm', '::save', '::rebuild'],
-        '#attributes' => ['class' => ['webform-button--draft', 'js-webform-novalidate']],
+        '#attributes' => [
+          'formnovalidate' => 'formnovalidate',
+          'class' => ['webform-button--draft'],
+        ],
         '#weight' => -10,
       ];
     }
@@ -1074,7 +1289,10 @@ protected function actions(array $form, FormStateInterface $form_state) {
         '#value' => $this->config('webform.settings')->get('settings.default_reset_button_label'),
         '#validate' => ['::noValidate'],
         '#submit' => ['::reset'],
-        '#attributes' => ['class' => ['webform-button--reset', 'js-webform-novalidate']],
+        '#attributes' => [
+          'formnovalidate' => 'formnovalidate',
+          'class' => ['webform-button--reset'],
+        ],
         '#weight' => 10,
       ];
     }
@@ -1084,6 +1302,20 @@ protected function actions(array $form, FormStateInterface $form_state) {
     return $element;
   }
 
+  /**
+   * Webform submission handler for the 'goto' action.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function gotoPage(array &$form, FormStateInterface $form_state) {
+    $element = $form_state->getTriggeringElement();
+    $form_state->set('current_page', $element['#page']);
+    $this->wizardSubmit($form, $form_state);
+  }
+
   /**
    * Webform submission handler for the 'next' action.
    *
@@ -1097,8 +1329,15 @@ public function next(array &$form, FormStateInterface $form_state) {
       return;
     }
     $pages = $this->getPages($form, $form_state);
+
+    // Get next page.
     $current_page = $this->getCurrentPage($form, $form_state);
-    $form_state->set('current_page', $this->getNextPage($pages, $current_page));
+    $next_page = $this->getNextPage($pages, $current_page);
+
+    // Set next page.
+    $form_state->set('current_page', $next_page);
+
+    // Submit next page.
     $this->wizardSubmit($form, $form_state);
   }
 
@@ -1112,8 +1351,15 @@ public function next(array &$form, FormStateInterface $form_state) {
    */
   public function previous(array &$form, FormStateInterface $form_state) {
     $pages = $this->getPages($form, $form_state);
+
+    // Get previous page.
     $current_page = $this->getCurrentPage($form, $form_state);
-    $form_state->set('current_page', $this->getPreviousPage($pages, $current_page));
+    $previous_page = $this->getPreviousPage($pages, $current_page);
+
+    // Set previous page.
+    $form_state->set('current_page', $previous_page);
+
+    // Submit previous page.
     $this->wizardSubmit($form, $form_state);
   }
 
@@ -1135,7 +1381,7 @@ protected function wizardSubmit(array &$form, FormStateInterface $form_state) {
       $this->confirmForm($form, $form_state);
     }
     elseif ($this->draftEnabled() && $this->getWebformSetting('draft_auto_save') && !$this->entity->isCompleted()) {
-      $form_state->setValue('in_draft', TRUE);
+      $form_state->set('in_draft', TRUE);
 
       $this->submitForm($form, $form_state);
       $this->save($form, $form_state);
@@ -1145,6 +1391,25 @@ protected function wizardSubmit(array &$form, FormStateInterface $form_state) {
       $this->submitForm($form, $form_state);
       $this->rebuild($form, $form_state);
     }
+
+    // Announce current page with progress.
+    // @see template_preprocess_webform_progress()
+    if ($this->isAjax()) {
+      $pages = $this->getPages($form, $form_state);
+
+      $page_keys = array_keys($pages);
+      $page_indexes = array_flip($page_keys);
+      $current_index = $page_indexes[$current_page];
+      $total_pages = count($page_keys);
+
+      $t_args = [
+        '@title' => $this->getWebform()->label(),
+        '@page' => $pages[$current_page]['#title'],
+        '@start' => ($current_index + 1),
+        '@end' => $total_pages,
+      ];
+      $this->announce($this->t('"@title: @page" loaded. (Page @start of @end)', $t_args));
+    }
   }
 
   /**
@@ -1158,7 +1423,7 @@ protected function wizardSubmit(array &$form, FormStateInterface $form_state) {
   public function autosave(array &$form, FormStateInterface $form_state) {
     if ($form_state->hasAnyErrors()) {
       if ($this->draftEnabled() && $this->getWebformSetting('draft_auto_save') && !$this->entity->isCompleted()) {
-        $form_state->setValue('in_draft', TRUE);
+        $form_state->set('in_draft', TRUE);
 
         $this->submitForm($form, $form_state);
         $this->save($form, $form_state);
@@ -1177,7 +1442,7 @@ public function autosave(array &$form, FormStateInterface $form_state) {
    */
   public function draft(array &$form, FormStateInterface $form_state) {
     $form_state->clearErrors();
-    $form_state->setValue('in_draft', TRUE);
+    $form_state->set('in_draft', TRUE);
     $form_state->set('draft_saved', TRUE);
     $this->entity->validate();
   }
@@ -1191,7 +1456,7 @@ public function draft(array &$form, FormStateInterface $form_state) {
    *   The current state of the form.
    */
   public function complete(array &$form, FormStateInterface $form_state) {
-    $form_state->setValue('in_draft', FALSE);
+    $form_state->set('in_draft', FALSE);
   }
 
   /**
@@ -1209,7 +1474,7 @@ public function complete(array &$form, FormStateInterface $form_state) {
    * More complex (web)form elements user #validate callbacks
    * to process and alter an element's submitted value. Element's that rely on
    * #validate to alter the submitted value include 'Password Confirm',
-   * 'Email Confirm', 'Composite Elements', 'Other Elements', and more...
+   * 'Email Confirm', 'Composite Elements', 'Other Elements', and more…
    *
    * If the #limit_validation_errors property is used within a multi-step wizard
    * form, previously submitted values will be corrupted.
@@ -1247,6 +1512,12 @@ public function rebuild(array &$form, FormStateInterface $form_state) {
   public function validateForm(array &$form, FormStateInterface $form_state) {
     parent::validateForm($form, $form_state);
 
+    // Disable inline form error when performing validation via the API.
+    if ($this->operation === 'api') {
+      // @see \Drupal\webform\WebformSubmissionForm::submitWebformSubmission
+      $form['#disable_inline_form_errors'] = TRUE;
+    }
+
     // Build webform submission with validated and processed data.
     $this->entity = $this->buildEntity($form, $form_state);
 
@@ -1271,9 +1542,47 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
       });
       // @see \Drupal\Core\Form\FormValidator::executeValidateHandlers
       foreach ($handlers as $callback) {
-        call_user_func_array($form_state->prepareCallback($callback), [&$form, &$form_state]);
+        $arguments = [&$form, &$form_state];
+        call_user_func_array($form_state->prepareCallback($callback), $arguments);
+      }
+    }
+
+    // Validate file (upload) limit.
+    // @see \Drupal\webform\Plugin\WebformElement\WebformManagedFileBase::validateManagedFileLimit
+    $file_limit = $this->getWebform()->getSetting('form_file_limit')
+      ?: $this->configFactory->get('webform.settings')->get('settings.default_form_file_limit')
+      ?: '';
+    $file_limit = Bytes::toInt($file_limit);
+    if (!$file_limit) {
+      return;
+    }
+
+    // Validate file upload limit.
+    $file_names = [];
+    $total_file_size = 0;
+    $element_keys = $this->getWebform()->getElementsManagedFiles();
+    foreach ($element_keys as $element_key) {
+      $data = $this->entity->getElementData($element_key);
+      if ($data) {
+        $fids = (array) $data;
+        /** @var \Drupal\file\FileInterface[] $files */
+        $files = $this->entityTypeManager->getStorage('file')->loadMultiple($fids);
+        foreach ($files as $file) {
+          $total_file_size += (int) $file->getSize();
+          $file_names[] = $file->getFilename() . ' - ' . format_size($file->getSize(), $this->entity->language()->getId());
+        }
       }
     }
+    if ($total_file_size > $file_limit) {
+      $t_args = ['%quota' => format_size($file_limit)];
+      $message = [];
+      $message['content'] = ['#markup' => $this->t("This form's file upload quota of %quota has been exceeded. Please remove some files.", $t_args)];
+      $message['files'] = [
+        '#theme' => 'item_list',
+        '#items' => $file_names,
+      ];
+      $form_state->setErrorByName(NULL, $this->renderer->renderPlain($message));
+    }
   }
 
   /**
@@ -1281,6 +1590,10 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
     parent::submitForm($form, $form_state);
+
+    // Server side #states API submit.
+    $this->conditionsValidator->submitForm($form, $form_state);
+
     // Submit webform via webform handler.
     $this->getWebform()->invokeHandlers('submitForm', $form, $form_state, $this->entity);
   }
@@ -1302,17 +1615,26 @@ public function confirmForm(array &$form, FormStateInterface $form_state) {
     // Confirm webform via webform handler.
     $this->getWebform()->invokeHandlers('confirmForm', $form, $form_state, $webform_submission);
 
-    // Reset the form if reloading the current form via AJAX, and just displaying a message.
+    // Get confirmation type.
     $confirmation_type = $this->getWebformSetting('confirmation_type');
+
+    // Rebuild or reset the form if reloading the current form via AJAX.
     if ($this->isAjax()) {
+      // On update, rebuild and display message unless ?destination= is set.
+      // @see \Drupal\webform\WebformSubmissionForm::setConfirmation
       $state = $webform_submission->getState();
-      if ($confirmation_type == WebformInterface::CONFIRMATION_MESSAGE || $state == WebformSubmissionInterface::STATE_UPDATED) {
+      if ($state === WebformSubmissionInterface::STATE_UPDATED) {
+        if (!$this->getRequest()->get('destination')) {
+          static::rebuild($form, $form_state);
+        }
+      }
+      elseif ($confirmation_type === WebformInterface::CONFIRMATION_MESSAGE || $confirmation_type === WebformInterface::CONFIRMATION_NONE) {
         static::reset($form, $form_state);
       }
     }
 
     // Always reset the form to trigger a modal dialog.
-    if ($confirmation_type == WebformInterface::CONFIRMATION_MODAL) {
+    if ($confirmation_type === WebformInterface::CONFIRMATION_MODAL) {
       static::reset($form, $form_state);
     }
   }
@@ -1328,7 +1650,7 @@ public function save(array $form, FormStateInterface $form_state) {
     // Make sure the uri and remote addr are set correctly because
     // Ajax requests can cause these values to be reset.
     if ($webform_submission->isNew()) {
-      if (preg_match('/\.webform\.test$/', $this->getRouteMatch()->getRouteName())) {
+      if (preg_match('/\.webform\.test_form$/', $this->getRouteMatch()->getRouteName())) {
         // For test submissions use the source URL.
         $source_url = $webform_submission->set('uri', NULL)->getSourceUrl()->setAbsolute(FALSE);
         $uri = preg_replace('#^' . base_path() . '#', '/', $source_url->toString());
@@ -1342,12 +1664,9 @@ public function save(array $form, FormStateInterface $form_state) {
         $uri = preg_replace('/\?$/', '', $uri);
       }
       $webform_submission->set('uri', $uri);
+      $webform_submission->set('remote_addr', ($this->getWebform()->hasRemoteAddr()) ? $this->getRequest()->getClientIp() : '');
       if ($this->isConfidential()) {
         $webform_submission->setOwnerId(0);
-        $webform_submission->set('remote_addr', '');
-      }
-      else {
-        $webform_submission->set('remote_addr', $this->getRequest()->getClientIp());
       }
     }
 
@@ -1375,6 +1694,13 @@ public function save(array $form, FormStateInterface $form_state) {
    *   The current state of the form.
    */
   public function reset(array &$form, FormStateInterface $form_state) {
+    // Delete save draft.
+    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
+    $webform_submission = $this->getEntity();
+    if ($webform_submission->isDraft()) {
+      $webform_submission->delete();
+    }
+
     // Create new webform submission.
     /** @var \Drupal\webform\Entity\WebformSubmission $webform_submission */
     $webform_submission = $this->getEntity()->createDuplicate();
@@ -1435,10 +1761,10 @@ protected function setFormPropertiesFromElements(array &$form, array &$elements)
   /****************************************************************************/
 
   /**
-   * Determine if this is a multistep wizard form.
+   * Determine if this is a multi-step wizard form.
    *
    * @return bool
-   *   TRUE if this multistep wizard form.
+   *   TRUE if this multi-step wizard form.
    */
   protected function hasPages() {
     return $this->getWebform()->getPages($this->operation);
@@ -1505,7 +1831,7 @@ protected function getCurrentPage(array &$form, FormStateInterface $form_state)
       }
       else {
         $current_page = $this->entity->getCurrentPage();
-        if ($current_page && isset($pages[$current_page]) && $this->draftEnabled()) {
+        if ($current_page && isset($pages[$current_page]) && !$this->entity->isCompleted()) {
           $form_state->set('current_page', $current_page);
         }
         else {
@@ -1701,7 +2027,7 @@ protected function setConfirmation(FormStateInterface $form_state) {
         $confirmation_url = $this->aliasManager->getPathByAlias($confirmation_url);
         if ($redirect_url = $this->pathValidator->getUrlIfValid($confirmation_url)) {
           if ($confirmation_type == WebformInterface::CONFIRMATION_URL_MESSAGE) {
-            $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION);
+            $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION_MESSAGE);
           }
           $this->setTrustedRedirectUrl($form_state, $redirect_url);
           return;
@@ -1713,14 +2039,14 @@ protected function setConfirmation(FormStateInterface $form_state) {
           ];
           // Display warning to use who can update the webform.
           if ($webform->access('update')) {
-            drupal_set_message($this->t('Confirmation URL %url is not valid.', $t_args), 'warning');
+            $this->messenger()->addWarning($this->t('Confirmation URL %url is not valid.', $t_args));
           }
           // Log warning.
           $this->getLogger('webform')->warning('@webform: Confirmation URL %url is not valid.', $t_args);
         }
 
         // If confirmation URL is invalid display message.
-        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION);
+        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION_MESSAGE);
         $route_options['query']['webform_id'] = $webform->id();
         $form_state->setRedirect($route_name, $route_parameters, $route_options);
         return;
@@ -1731,11 +2057,11 @@ protected function setConfirmation(FormStateInterface $form_state) {
         return;
 
       case WebformInterface::CONFIRMATION_MESSAGE:
-        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION);
+        $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION_MESSAGE);
         return;
 
       case WebformInterface::CONFIRMATION_MODAL:
-        $message = $this->getMessageManager()->build(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION);
+        $message = $this->getMessageManager()->build(WebformMessageManagerInterface::SUBMISSION_CONFIRMATION_MESSAGE);
         if ($message) {
           // Set webform confirmation modal in $form_state.
           $form_state->set('webform_confirmation_modal', [
@@ -1745,6 +2071,9 @@ protected function setConfirmation(FormStateInterface $form_state) {
         }
         return;
 
+      case WebformInterface::CONFIRMATION_NONE:
+        return;
+
       case WebformInterface::CONFIRMATION_DEFAULT:
       default:
         $this->getMessageManager()->display(WebformMessageManagerInterface::SUBMISSION_DEFAULT_CONFIRMATION);
@@ -1752,6 +2081,21 @@ protected function setConfirmation(FormStateInterface $form_state) {
     }
   }
 
+  /**
+   * Hide confirmation modal during form validation.
+   *
+   * This prevent duplicate modal dialog from appearing.
+   */
+  public static function removeConfirmationModal(&$element, FormStateInterface $form_state, &$complete_form) {
+    // Reset confirmation modal.
+    $storage = $form_state->getStorage();
+    unset($storage['webform_confirmation_modal']);
+    $form_state->setStorage($storage);
+
+    // Remove modal from form.
+    unset($complete_form['webform_confirmation_modal']);
+  }
+
   /****************************************************************************/
   // Elements functions
   /****************************************************************************/
@@ -1764,7 +2108,7 @@ protected function setConfirmation(FormStateInterface $form_state) {
    */
   protected function hideElements(array &$elements) {
     foreach ($elements as $key => &$element) {
-      if (Element::property($key) || !is_array($element)) {
+      if (!WebformElementHelper::isElement($element, $key)) {
         continue;
       }
 
@@ -1787,18 +2131,49 @@ protected function hideElements(array &$elements) {
    */
   protected function prepareElements(array &$elements, array &$form, FormStateInterface $form_state) {
     foreach ($elements as $key => &$element) {
-      if (Element::property($key) || !is_array($element)) {
+      if (!WebformElementHelper::isElement($element, $key)) {
         continue;
       }
 
       // Build the webform element.
       $this->elementManager->buildElement($element, $form, $form_state);
 
+      if (isset($element['#states'])) {
+        $element['#states'] = $this->addStatesPrefix($element['#states']);
+      }
+
       // Recurse and prepare nested elements.
       $this->prepareElements($element, $form, $form_state);
     }
   }
 
+  /**
+   * Add unique class prefix to all :input #states selectors.
+   *
+   * @param array $array
+   *   An associative array.
+   *
+   * @return array
+   *   An associative array with unique class prefix added to all :input
+   *   #states selectors.
+   */
+  protected function addStatesPrefix(array $array) {
+    $prefixed_array = [];
+    foreach ($array as $key => $value) {
+      if (strpos($key, ':input') === 0) {
+        $key = $this->statesPrefix . ' ' . $key;
+        $prefixed_array[$key] = $value;
+      }
+      elseif (is_array($value)) {
+        $prefixed_array[$key] = $this->addStatesPrefix($value);
+      }
+      else {
+        $prefixed_array[$key] = $value;
+      }
+    }
+    return $prefixed_array;
+  }
+
   /**
    * Prepopulate element data.
    *
@@ -1829,7 +2204,7 @@ protected function prepopulateData(array &$data) {
    */
   protected function populateElements(array &$elements, array $values) {
     foreach ($elements as $key => &$element) {
-      if (Element::property($key) || !is_array($element)) {
+      if (!WebformElementHelper::isElement($element, $key)) {
         continue;
       }
 
@@ -1890,11 +2265,18 @@ protected function populateElements(array &$elements, array $values) {
   protected function checkTotalLimit() {
     $webform = $this->getWebform();
 
+    // Get limit total to unique submission per webform/source entity.
+    $limit_total_unique = $this->getWebformSetting('limit_total_unique');
+
     // Check per source entity total limit.
     $entity_limit_total = $this->getWebformSetting('entity_limit_total');
     $entity_limit_total_interval = $this->getWebformSetting('entity_limit_total_interval');
+    if ($limit_total_unique) {
+      $entity_limit_total = 1;
+      $entity_limit_total_interval = NULL;
+    }
     if ($entity_limit_total && ($source_entity = $this->getLimitSourceEntity())) {
-      if ($this->storage->getTotal($webform, $source_entity, NULL, ['interval' => $entity_limit_total_interval]) >= $entity_limit_total) {
+      if ($this->getStorage()->getTotal($webform, $source_entity, NULL, ['interval' => $entity_limit_total_interval]) >= $entity_limit_total) {
         return TRUE;
       }
     }
@@ -1902,7 +2284,11 @@ protected function checkTotalLimit() {
     // Check total limit.
     $limit_total = $this->getWebformSetting('limit_total');
     $limit_total_interval = $this->getWebformSetting('limit_total_interval');
-    if ($limit_total && $this->storage->getTotal($webform, NULL, NULL, ['interval' => $limit_total_interval]) >= $limit_total) {
+    if ($limit_total_unique) {
+      $limit_total = 1;
+      $limit_total_interval = NULL;
+    }
+    if ($limit_total && $this->getStorage()->getTotal($webform, NULL, NULL, ['interval' => $limit_total_interval]) >= $limit_total) {
       return TRUE;
     }
 
@@ -1919,17 +2305,8 @@ protected function checkUserLimit() {
     // Allow anonymous and authenticated users edit own submission.
     /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
     $webform_submission = $this->getEntity();
-    if ($webform_submission->id()) {
-      if ($this->currentUser()->isAnonymous()) {
-        if (!empty($_SESSION['webform_submissions']) && in_array($webform_submission->id(), $_SESSION['webform_submissions'])) {
-          return FALSE;
-        }
-      }
-      else {
-        if ($webform_submission->getOwnerId() === $this->currentUser()->id()) {
-          return FALSE;
-        }
-      }
+    if ($webform_submission->id() && $webform_submission->isOwner($this->currentUser())) {
+      return FALSE;
     }
 
     // Get the submission owner and not current user.
@@ -1942,7 +2319,7 @@ protected function checkUserLimit() {
     $entity_limit_user = $this->getWebformSetting('entity_limit_user');
     $entity_limit_user_interval = $this->getWebformSetting('entity_limit_user_interval');
     if ($entity_limit_user && ($source_entity = $this->getLimitSourceEntity())) {
-      if ($this->storage->getTotal($webform, $source_entity, $account, ['interval' => $entity_limit_user_interval]) >= $entity_limit_user) {
+      if ($this->getStorage()->getTotal($webform, $source_entity, $account, ['interval' => $entity_limit_user_interval]) >= $entity_limit_user) {
         return TRUE;
       }
     }
@@ -1950,7 +2327,7 @@ protected function checkUserLimit() {
     // Check user limit.
     $limit_user = $this->getWebformSetting('limit_user');
     $limit_user_interval = $this->getWebformSetting('limit_user_interval');
-    if ($limit_user && $this->storage->getTotal($webform, NULL, $account, ['interval' => $limit_user_interval]) >= $limit_user) {
+    if ($limit_user && $this->getStorage()->getTotal($webform, NULL, $account, ['interval' => $limit_user_interval]) >= $limit_user) {
       return TRUE;
     }
 
@@ -2066,26 +2443,13 @@ protected function isWebformEntityReferenceFromSourceEntity() {
   // Helper functions
   /****************************************************************************/
 
-  /**
-   * Get the message manager.
-   *
-   * We need to wrap the message manager service because the webform submission
-   * entity is being continuous cloned and updated during form processing.
-   *
-   * @see \Drupal\Core\Entity\EntityForm::buildEntity
-   */
-  protected function getMessageManager() {
-    $this->messageManager->setWebformSubmission($this->getEntity());
-    return $this->messageManager;
-  }
-
   /**
    * Get the webform submission's webform.
    *
    * @return \Drupal\webform\WebformInterface
    *   A webform.
    */
-  protected function getWebform() {
+  public function getWebform() {
     /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
     $webform_submission = $this->getEntity();
     return $webform_submission->getWebform();
@@ -2101,6 +2465,29 @@ protected function getSourceEntity() {
     return $this->sourceEntity;
   }
 
+  /**
+   * Get the webform submission entity storage.
+   *
+   * @return \Drupal\Webform\WebformSubmissionStorageInterface
+   *   The webform submission entity storage.
+   */
+  protected function getStorage() {
+    return $this->entityManager->getStorage('webform_submission');
+  }
+
+  /**
+   * Get the message manager.
+   *
+   * We need to wrap the message manager service because the webform submission
+   * entity is being continuous cloned and updated during form processing.
+   *
+   * @see \Drupal\Core\Entity\EntityForm::buildEntity
+   */
+  protected function getMessageManager() {
+    $this->messageManager->setWebformSubmission($this->getEntity());
+    return $this->messageManager;
+  }
+
   /**
    * Get source entity for use with entity limit total and user submissions.
    *
@@ -2132,7 +2519,7 @@ protected function getLastSubmission($completed = TRUE) {
     $source_entity = $this->getSourceEntity();
     $account = $this->getEntity()->getOwner();
     $options = ($completed) ? ['in_draft' => FALSE] : [];
-    return $this->storage->getLastSubmission($webform, $source_entity, $account, $options);
+    return $this->getStorage()->getLastSubmission($webform, $source_entity, $account, $options);
   }
 
   /**
@@ -2147,20 +2534,12 @@ protected function getLastSubmission($completed = TRUE) {
    *   A webform setting.
    */
   protected function getWebformSetting($name, $default_value = NULL) {
-    // Get webform settings with default values.
-    if (empty($this->settings)) {
-      $this->settings = $this->getWebform()->getSettings();
-      $default_settings = $this->config('webform.settings')->get('settings');
-      foreach ($default_settings as $key => $value) {
-        $key = str_replace('default_', '', $key);
-        if (empty($this->settings[$key])) {
-          $this->settings[$key] = $value;
-        }
-      }
-    }
+    $value = $this->getWebform()->getSetting($name)
+      ?: $this->config('webform.settings')->get('settings.default_' . $name)
+      ?: NULL;
 
-    if (isset($this->settings[$name])) {
-      return $this->tokenManager->replace($this->settings[$name], $this->getEntity());
+    if ($value !== NULL) {
+      return $this->tokenManager->replace($value, $this->getEntity());
     }
     else {
       return $default_value;
@@ -2302,12 +2681,28 @@ public static function submitWebformSubmission(WebformSubmissionInterface $webfo
     // form is submitted.
     $form_state = new FormState();
 
+    // Set the triggering element to an empty element to prevent
+    // errors from managed files.
+    // @see \Drupal\file\Element\ManagedFile::validateManagedFile
+    $form_state->setTriggeringElement(['#parents' => []]);
+
+    // Get existing error messages.
+    $error_messages = \Drupal::messenger()->messagesByType(MessengerInterface::TYPE_ERROR);
+
     // Submit the form.
     \Drupal::formBuilder()->submitForm($form_object, $form_state);
 
     // Get the errors but skip drafts.
     $errors = ($webform_submission->isDraft() && !$validate_only) ? [] : $form_state->getErrors();
 
+    // Delete all form related error messages.
+    \Drupal::messenger()->deleteByType(MessengerInterface::TYPE_ERROR);
+
+    // Restore existing error message.
+    foreach ($error_messages as $error_message) {
+      \Drupal::messenger()->addError($error_message);
+    }
+
     if ($errors) {
       return $errors;
     }
diff --git a/web/modules/webform/src/WebformSubmissionGenerate.php b/web/modules/webform/src/WebformSubmissionGenerate.php
index 62c1c313f14bc7ed59667b25399f5d0c238b034b..265c787c9bc890d22cf199fee9e4f1f975e17a71 100644
--- a/web/modules/webform/src/WebformSubmissionGenerate.php
+++ b/web/modules/webform/src/WebformSubmissionGenerate.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\webform;
 
-use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Form\OptGroup;
 use Drupal\Core\Serialization\Yaml;
 use Drupal\Core\Config\ConfigFactoryInterface;
@@ -116,9 +115,19 @@ public function getTestValue(WebformInterface $webform, $name, array $element, a
     }
 
     // Apply #maxlength to values.
+    // @see \Drupal\webform\Plugin\WebformElement\TextBase
     if (!empty($element['#maxlength'])) {
+      $maxlength = $element['#maxlength'];
+    }
+    elseif (!empty($element['#counter_type']) && !empty($element['#counter_maximum']) && $element['#counter_type'] === 'character') {
+      $maxlength = $element['#counter_maximum'];
+    }
+    else {
+      $maxlength = NULL;
+    }
+    if ($maxlength) {
       foreach ($values as $index => $value) {
-        $values[$index] = Unicode::substr($value, 0, $element['#maxlength']);
+        $values[$index] = mb_substr($value, 0, $maxlength);
       }
     }
 
@@ -170,11 +179,6 @@ protected function getTestValues(WebformInterface $webform, $name, array $elemen
       return $element['#test'];
     }
 
-    // Never populate hidden and value elements.
-    if (in_array($element['#type'], ['hidden', 'value'])) {
-      return NULL;
-    }
-
     // Invoke WebformElement::test and get a test value.
     // If test value is NULL this element should never be populated with
     // test data.
diff --git a/web/modules/webform/src/WebformSubmissionInterface.php b/web/modules/webform/src/WebformSubmissionInterface.php
index 45a5796f50faf4661b1ab969b9a5143cbf3d2ffc..1cc0d5679d763309822ceab34856bf81c73bb285 100644
--- a/web/modules/webform/src/WebformSubmissionInterface.php
+++ b/web/modules/webform/src/WebformSubmissionInterface.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\webform;
 
+use Drupal\Core\Session\AccountInterface;
 use Drupal\user\EntityOwnerInterface;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\user\UserInterface;
@@ -239,6 +240,17 @@ public function isCompleted();
    */
   public function isSticky();
 
+  /**
+   * Test whether the provided account is owner of this webform submission.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   Account whose ownership to test.
+   *
+   * @return bool
+   *   Whether the provided account is owner of this webform submission.
+   */
+  public function isOwner(AccountInterface $account);
+
   /**
    * Checks submission notes.
    *
@@ -334,10 +346,13 @@ public function getWebform();
   /**
    * Gets the webform submission's source entity.
    *
+   * @param bool $translate
+   *   (optional) If TRUE the source entity will be translated.
+   *
    * @return \Drupal\Core\Entity\EntityInterface|null
    *   The entity that this webform submission was created from.
    */
-  public function getSourceEntity();
+  public function getSourceEntity($translate = FALSE);
 
   /**
    * Gets the webform submission's source URL.
@@ -351,7 +366,7 @@ public function getSourceUrl();
    * Gets the webform submission's secure tokenized URL.
    *
    * @return \Drupal\Core\Url
-   *   The the webform submission's secure tokenized URL.
+   *   The webform submission's secure tokenized URL.
    */
   public function getTokenUrl();
 
@@ -372,7 +387,7 @@ public function invokeWebformHandlers($method);
   public function invokeWebformElements($method);
 
   /**
-   * Convert anonymous submission to authenicated.
+   * Convert anonymous submission to authenticated.
    *
    * @param \Drupal\user\UserInterface $account
    *   An authenticated user account.
diff --git a/web/modules/webform/src/WebformSubmissionListBuilder.php b/web/modules/webform/src/WebformSubmissionListBuilder.php
index 600a164b9e0a8d746b0967a37fcb2ef54b9e11ec..4e05ee416d1c2e1f1799f0fc9c6e55ce22fa7eb8 100644
--- a/web/modules/webform/src/WebformSubmissionListBuilder.php
+++ b/web/modules/webform/src/WebformSubmissionListBuilder.php
@@ -3,14 +3,26 @@
 namespace Drupal\webform;
 
 use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Database\Database;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityListBuilder;
 use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Link;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
+use Drupal\views\Views;
+use Drupal\webform\Controller\WebformSubmissionController;
+use Drupal\webform\Plugin\WebformElementManagerInterface;
+use Drupal\webform\Utility\WebformArrayHelper;
 use Drupal\webform\Utility\WebformDialogHelper;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
  * Provides a list controller for webform submission entity.
@@ -49,6 +61,41 @@ class WebformSubmissionListBuilder extends EntityListBuilder {
    */
   const STATE_DRAFT = 'draft';
 
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * The current request.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * The configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
   /**
    * The webform request handler.
    *
@@ -56,6 +103,13 @@ class WebformSubmissionListBuilder extends EntityListBuilder {
    */
   protected $requestHandler;
 
+  /**
+   * The webform element manager.
+   *
+   * @var \Drupal\webform\Plugin\WebformElementManagerInterface
+   */
+  protected $elementManager;
+
   /**
    * The webform message manager.
    *
@@ -84,6 +138,14 @@ class WebformSubmissionListBuilder extends EntityListBuilder {
    */
   protected $account;
 
+  /**
+   * Draft flag.
+   *
+   * @var bool
+   */
+  protected $draft;
+
+
   /**
    * The columns being displayed.
    *
@@ -108,6 +170,13 @@ class WebformSubmissionListBuilder extends EntityListBuilder {
     'element_format' => 'value',
   ];
 
+  /**
+   * The submission link type. (canonical, table, or edit)
+   *
+   * @var array
+   */
+  protected $linkType = 'canonical';
+
   /**
    * The webform elements.
    *
@@ -129,6 +198,13 @@ class WebformSubmissionListBuilder extends EntityListBuilder {
    */
   protected $sort;
 
+  /**
+   * Search source entity.
+   *
+   * @var string
+   */
+  protected $sourceEntityTypeId;
+
   /**
    * Sort direction.
    *
@@ -158,99 +234,194 @@ class WebformSubmissionListBuilder extends EntityListBuilder {
   protected $customize;
 
   /**
-   * The webform element manager.
+   * The name of current submission view.
    *
-   * @var \Drupal\webform\Plugin\WebformElementManagerInterface
+   * @var string
    */
-  protected $elementManager;
+  protected $submissionView;
+
+  /**
+   * An associative array of submission views.
+   *
+   * @var string
+   */
+  protected $submissionViews;
 
   /**
    * {@inheritdoc}
    */
-  public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage) {
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('entity.manager')->getStorage($entity_type->id()),
+      $container->get('entity.manager'),
+      $container->get('current_route_match'),
+      $container->get('request_stack'),
+      $container->get('current_user'),
+      $container->get('config.factory'),
+      $container->get('webform.request'),
+      $container->get('plugin.manager.webform.element'),
+      $container->get('webform.message_manager')
+    );
+  }
+
+  /**
+   * Constructs a new WebformSubmissionListBuilder object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   *   The entity storage class.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The configuration factory.
+   * @param \Drupal\webform\WebformRequestInterface $webform_request
+   *   The webform request handler.
+   * @param \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager
+   *   The webform element manager.
+   * @param \Drupal\webform\WebformMessageManagerInterface $message_manager
+   *   The webform message manager.
+   */
+  public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, EntityTypeManagerInterface $entity_type_manager, RouteMatchInterface $route_match, RequestStack $request_stack, AccountInterface $current_user, ConfigFactoryInterface $config_factory, WebformRequestInterface $webform_request, WebformElementManagerInterface $element_manager, WebformMessageManagerInterface $message_manager) {
     parent::__construct($entity_type, $storage);
+    $this->entityTypeManager = $entity_type_manager;
+    $this->routeMatch = $route_match;
+    $this->request = $request_stack->getCurrentRequest();
+    $this->currentUser = $current_user;
+    $this->configFactory = $config_factory;
+    $this->requestHandler = $webform_request;
+    $this->elementManager = $element_manager;
+    $this->messageManager = $message_manager;
+
+    $this->keys = $this->request->query->get('search');
+    $this->state = $this->request->query->get('state');
+    $this->sourceEntityTypeId = $this->request->query->get('entity');
 
-    $this->requestHandler = \Drupal::service('webform.request');
+    list($this->webform, $this->sourceEntity) = $this->requestHandler->getWebformEntities();
 
-    $this->keys = \Drupal::request()->query->get('search');
-    $this->state = \Drupal::request()->query->get('state');
+    $this->messageManager->setWebform($this->webform);
+    $this->messageManager->setSourceEntity($this->sourceEntity);
 
-    list($this->webform, $this->sourceEntity) = $this->requestHandler->getWebformEntities();
+    /** @var WebformSubmissionStorageInterface $webform_submission_storage */
+    $webform_submission_storage = $this->getStorage();
 
+    $route_name = $this->routeMatch->getRouteName();
     $base_route_name = ($this->webform) ? $this->requestHandler->getBaseRouteName($this->webform, $this->sourceEntity) : '';
-    if (in_array(\Drupal::routeMatch()->getRouteName(), ["$base_route_name.webform.user.submissions", "$base_route_name.webform.user.drafts"])) {
-      $this->account = \Drupal::currentUser();
-      // Set submission filter so that we can support user.submissions and
-      // user.drafts routes.
-      $this->state = (\Drupal::routeMatch()->getRouteName() === "$base_route_name.webform.user.submissions") ? self::STATE_COMPLETED : self::STATE_DRAFT;
+
+    // Set account and state based on the current route.
+    switch ($route_name) {
+      case 'entity.webform_submission.user':
+        $this->account = $this->routeMatch->getParameter('user');
+        break;
+
+      case "$base_route_name.webform.user.submissions":
+        $this->account = $this->currentUser;
+        $this->draft = FALSE;
+        break;
+
+      case "$base_route_name.webform.user.drafts":
+        $this->account = $this->currentUser;
+        $this->draft = TRUE;
+        break;
+
+      default:
+        $this->account = NULL;
+        break;
     }
-    else {
-      $this->account = NULL;
+
+    // Initialize submission views and view.
+    $this->submissionView = $this->routeMatch->getParameter('submission_view') ?: '';
+    $this->submissionViews = $this->getSubmissionViews();
+    if ($this->submissionViews && empty($this->submissionView) && $this->isSubmissionViewResultsReplaced()) {
+      $this->submissionView = WebformArrayHelper::getFirstKey($this->submissionViews);
     }
 
-    $this->elementManager = \Drupal::service('plugin.manager.webform.element');
+    // Check submission view access.
+    if ($this->submissionView && !isset($this->submissionViews[$this->submissionView])) {
+      $submission_views = $this->getSubmissionViewsConfig();
+      if (isset($submission_views[$this->submissionView])) {
+        throw new AccessDeniedHttpException();
+      }
+      else {
+        throw new NotFoundHttpException();
+      }
+    }
 
-    /** @var \Drupal\webform\WebformMessageManagerInterface $message_manager */
-    $this->messageManager = \Drupal::service('webform.message_manager');
-    $this->messageManager->setWebform($this->webform);
-    $this->messageManager->setSourceEntity($this->sourceEntity);
+    // If there is a submission view, we do not need to initialize
+    // the entity list.
+    if ($this->submissionView) {
+      return;
+    }
 
-    /** @var WebformSubmissionStorageInterface $webform_submission_storage */
-    $webform_submission_storage = $this->getStorage();
+    // Set default display settings.
+    $this->direction = 'desc';
+    $this->limit = 20;
+    $this->customize = FALSE;
+    $this->sort = 'created';
+    $this->total = $this->getTotal($this->keys, $this->state, $this->sourceEntityTypeId);
 
-    $route_name = \Drupal::routeMatch()->getRouteName();
-    if ($route_name == "$base_route_name.webform.results_submissions") {
-      // Display submission properties and elements.
+    switch ($route_name) {
+      // Display webform submissions which includes properties and elements.
       // @see /admin/structure/webform/manage/{webform}/results/submissions
       // @see /node/{node}/webform/results/submissions
-      $this->columns = $webform_submission_storage->getCustomColumns($this->webform, $this->sourceEntity, $this->account, TRUE);
-      $this->sort = $webform_submission_storage->getCustomSetting('sort', 'serial', $this->webform, $this->sourceEntity);
-      $this->direction = $webform_submission_storage->getCustomSetting('direction', 'desc', $this->webform, $this->sourceEntity);
-      $this->limit = $webform_submission_storage->getCustomSetting('limit', 50, $this->webform, $this->sourceEntity);
-      $this->format = $webform_submission_storage->getCustomSetting('format', $this->format, $this->webform, $this->sourceEntity);
-      $this->customize = $this->webform->access('update');
-      if ($this->format['element_format'] == 'raw') {
-        foreach ($this->columns as &$column) {
-          $column['format'] = 'raw';
-          if (isset($column['element'])) {
-            $column['element']['#format'] = 'raw';
+      case "$base_route_name.webform.results_submissions":
+        $this->columns = $webform_submission_storage->getCustomColumns($this->webform, $this->sourceEntity, $this->account, TRUE);
+        $this->sort = $webform_submission_storage->getCustomSetting('sort', 'created', $this->webform, $this->sourceEntity);
+        $this->direction = $webform_submission_storage->getCustomSetting('direction', 'desc', $this->webform, $this->sourceEntity);
+        $this->limit = $webform_submission_storage->getCustomSetting('limit', 20, $this->webform, $this->sourceEntity);
+        $this->format = $webform_submission_storage->getCustomSetting('format', $this->format, $this->webform, $this->sourceEntity);
+        $this->linkType = $webform_submission_storage->getCustomSetting('link_type', $this->linkType, $this->webform, $this->sourceEntity);
+        $this->customize = $this->webform->access('update');
+        if ($this->format['element_format'] == 'raw') {
+          foreach ($this->columns as &$column) {
+            $column['format'] = 'raw';
+            if (isset($column['element'])) {
+              $column['element']['#format'] = 'raw';
+            }
           }
         }
-      }
-    }
-    else {
-      if ($route_name == 'entity.webform_submission.collection') {
+        break;
+
+      // Display all submissions.
+      // @see /admin/structure/webform/submissions/manage
+      case 'entity.webform_submission.collection':
         // Display only submission properties.
         // @see /admin/structure/webform/submissions/manage
-        $this->columns = $webform_submission_storage->getDefaultColumns($this->webform, $this->sourceEntity, $this->account, FALSE);
-        // Replace serial with sid when showing results from all webforms.
-        unset($this->columns['serial']);
-        $this->columns = [
-            'sid' => [
-              'title' => $this->t('SID'),
-              'name' => 'sid',
-              'format' => 'value',
-            ],
-          ] + $this->columns;
-        $this->sort = 'sid';
-      }
-      else {
+        $this->columns = $webform_submission_storage->getSubmissionsColumns();
+        break;
+
+      // Display user's submissions.
+      // @see /user/{user}/submissions
+      case 'entity.webform_submission.user':
+        $this->columns = $webform_submission_storage->getUsersSubmissionsColumns();
+        break;
+
+      // Display user's submissions.
+      // @see /webform/{webform}/submissions
+      // @see /webform/{webform}/drafts
+      // @see /node/{node}/webform/submissions
+      // @see /node/{node}/webform/drafts
+      case "$base_route_name.webform.user.drafts":
+      case "$base_route_name.webform.user.submissions":
+      default:
         $this->columns = $webform_submission_storage->getUserColumns($this->webform, $this->sourceEntity, $this->account, TRUE);
-        unset($this->columns['sid']);
-        $this->sort = 'serial';
-      }
-      $this->direction = 'desc';
-      $this->limit = 50;
-      $this->customize = FALSE;
+        break;
     }
-
-    $this->total = $this->getTotal($this->keys, $this->state);
   }
 
   /**
    * {@inheritdoc}
    */
   public function render() {
+    $build = [];
+
     // Set user specific page title.
     if ($this->webform && $this->account) {
       $t_args = [
@@ -264,6 +435,9 @@ public function render() {
         $build['#title'] = $this->t('Submissions to %webform for %user', $t_args);
       }
     }
+    elseif ($this->account) {
+      $build['#title'] = $this->account->getDisplayName();
+    }
 
     // Display warning when the webform has a submission but saving of results.
     // are disabled.
@@ -271,47 +445,123 @@ public function render() {
       $this->messageManager->display(WebformMessageManagerInterface::FORM_SAVE_EXCEPTION, 'warning');
     }
 
-    // Add the filter.
-    if (empty($this->account)) {
-      $state_options = [
-        '' => $this->t('All [@total]', ['@total' => $this->getTotal(NULL, NULL)]),
-        self::STATE_STARRED => $this->t('Starred [@total]', ['@total' => $this->getTotal(NULL, self::STATE_STARRED)]),
-        self::STATE_UNSTARRED => $this->t('Unstarred [@total]', ['@total' => $this->getTotal(NULL, self::STATE_UNSTARRED)]),
-        self::STATE_LOCKED => $this->t('Locked [@total]', ['@total' => $this->getTotal(NULL, self::STATE_LOCKED)]),
-        self::STATE_UNLOCKED => $this->t('Unlocked [@total]', ['@total' => $this->getTotal(NULL, self::STATE_UNLOCKED)]),
-      ];
-      // Add draft to state options.
-      if (!$this->webform || $this->webform->getSetting('draft') != WebformInterface::DRAFT_NONE) {
-        $state_options += [
-          self::STATE_COMPLETED => $this->t('Completed [@total]', ['@total' => $this->getTotal(NULL, self::STATE_COMPLETED)]),
-          self::STATE_DRAFT => $this->t('Draft [@total]', ['@total' => $this->getTotal(NULL, self::STATE_DRAFT)]),
-        ];
+    $build += $this->buildSubmissionViewsMenu();
+
+    if ($this->submissionView) {
+      $build += $this->buildSubmissionViews();
+    }
+    else {
+      $build += $this->buildEntityList();
+    }
+
+    $build['#attached']['library'][] = 'webform/webform.admin';
+
+    return $build;
+  }
+
+  /**
+   * Build the webform submission view.
+   *
+   * @return array
+   *   A renderable array containing a submission view.
+   */
+  protected function buildSubmissionViews() {
+    $settings = $this->submissionViews[$this->submissionView];
+
+    // Get view name and display id.
+    list($name, $display_id) = explode(':', $settings['view']);
+
+    // Load the view and set custom property used to fix the exposed
+    // filter action.
+    // @see webform_form_views_exposed_form_alter()
+    $view = Views::getView($name);
+    $view->webform_submission_view = TRUE;
+
+    // Get the current display or default arguments.
+    $displays = $view->storage->get('display');
+    if (!empty($displays[$display_id]['display_options']['arguments'])) {
+      $display_arguments = $displays[$display_id]['display_options']['arguments'];
+    }
+    elseif (!empty($displays['default']['display_options']['arguments'])) {
+      $display_arguments = $displays['default']['display_options']['arguments'];
+    }
+    else {
+      $display_arguments = [];
+    }
+
+    // Populate the views arguments.
+    $arguments = [];
+    foreach ($display_arguments as $argument_name => $display_argument) {
+      if ($display_argument['table'] !== 'webform_submission') {
+        $arguments[] = 'all';
+      }
+      else {
+        switch ($argument_name) {
+          case 'webform_id':
+            $arguments[] = (isset($this->webform)) ? $this->webform->id() : 'all';
+            break;
+
+          case 'entity_type':
+            $arguments[] = (isset($this->sourceEntity)) ? $this->sourceEntity->getEntityTypeId() : 'all';
+            break;
+
+          case 'entity_id':
+            $arguments[] = (isset($this->sourceEntity)) ? $this->sourceEntity->id() : 'all';
+            break;
+
+          case 'uid':
+            $arguments[] = (isset($this->account)) ? $this->account->id() : 'all';
+            break;
+
+          case 'in_draft':
+            $arguments[] = (isset($this->draft)) ? ($this->draft ? '1' : '0') : 'all';
+            break;
+
+          default:
+            $arguments[] = 'all';
+            break;
+        }
       }
-      $build['filter_form'] = \Drupal::formBuilder()
-        ->getForm('\Drupal\webform\Form\WebformSubmissionFilterForm', $this->keys, $this->state, $state_options);
     }
 
-    // Customize.
+    $build = [];
+    $build['view'] = [
+      '#type' => 'view',
+      '#view' => $view,
+      '#display_id' => $display_id,
+      '#arguments' => $arguments,
+    ];
+    return $build;
+  }
+
+  /**
+   * Build the webform submission entity list.
+   *
+   * @return array
+   *   A renderable array containing the entity list.
+   */
+  protected function buildEntityList() {
+    $build = [];
+
+    // Filter form.
+    if (empty($this->account)) {
+      $build['filter_form'] = $this->buildFilterForm();
+    }
+
+    // Customize buttons.
     if ($this->customize) {
       $build['custom_top'] = $this->buildCustomizeButton();
     }
 
     // Display info.
     if ($this->total) {
-      if ($this->account && $this->state == self::STATE_DRAFT) {
-        $info = $this->formatPlural($this->total, '@total draft', '@total drafts', ['@total' => $this->total]);
-      }
-      else {
-        $info = $this->formatPlural($this->total, '@total submission', '@total submissions', ['@total' => $this->total]);
-      }
-      $build['info'] = [
-        '#markup' => $info,
-        '#prefix' => '<div>',
-        '#suffix' => '</div>',
-      ];
+      $build['info'] = $this->buildInfo();
     }
 
+    // Table.
     $build += parent::render();
+    $build['table']['#sticky'] = TRUE;
+    $build['table']['#attributes']['class'][] = 'webform-results-table';
 
     // Customize.
     // Only displayed when more than 20 submissions are being displayed.
@@ -322,16 +572,117 @@ public function render() {
       }
     }
 
-    $build['table']['#attributes']['class'][] = 'webform-results__table';
-
-    $build['#attached']['library'][] = 'webform/webform.admin';
-
     // Must preload libraries required by (modal) dialogs.
     WebformDialogHelper::attachLibraries($build);
 
     return $build;
   }
 
+  /**
+   * Build the submission views menu.
+   *
+   * @return array
+   *   A render array representing the submission views menu.
+   */
+  protected function buildSubmissionViewsMenu() {
+    if (empty($this->submissionViews)) {
+      return [];
+    }
+
+    $route_name = $this->routeMatch->getRouteName();
+    $route_parameters = $this->routeMatch->getRawParameters()->all();
+    unset($route_parameters['submission_view']);
+
+    $links = [];
+    if (!$this->isSubmissionViewResultsReplaced()) {
+      $links['_default_'] = [
+        'title' => $this->t('Submissions'),
+        'url' => Url::fromRoute($route_name, $route_parameters),
+      ];
+    }
+    foreach ($this->submissionViews as $name => $submission_view) {
+      $links[$name] = [
+        'title' => $submission_view['title'],
+        'url' => Url::fromRoute($route_name, $route_parameters + ['submission_view' => $name]),
+      ];
+    }
+
+    // Only display the submission views menu when there is more than 1 link.
+    if (count($links) <= 1) {
+      return [];
+    }
+
+    // Make sure the current submission view is first.
+    if ($this->submissionView) {
+      $links = [$this->submissionView => $links[$this->submissionView]] + $links;
+    }
+
+    $build = [];
+    $build['submission_views'] = [
+      '#type' => 'operations',
+      '#links' => $links,
+      '#prefix' => '<div class="webform-dropbutton webform-submission-views-dropbutton">',
+      '#suffix' => '</div>' . ($this->submissionView ? '<p><hr/></p>' : ''),
+    ];
+    return $build;
+  }
+
+  /**
+   * Build the filter form.
+   *
+   * @return array
+   *   A render array representing the filter form.
+   */
+  protected function buildFilterForm() {
+    // State options.
+    $state_options = [
+      '' => $this->t('All [@total]', ['@total' => $this->getTotal(NULL, NULL, $this->sourceEntityTypeId)]),
+      self::STATE_STARRED => $this->t('Starred [@total]', ['@total' => $this->getTotal(NULL, self::STATE_STARRED, $this->sourceEntityTypeId)]),
+      self::STATE_UNSTARRED => $this->t('Unstarred [@total]', ['@total' => $this->getTotal(NULL, self::STATE_UNSTARRED, $this->sourceEntityTypeId)]),
+      self::STATE_LOCKED => $this->t('Locked [@total]', ['@total' => $this->getTotal(NULL, self::STATE_LOCKED, $this->sourceEntityTypeId)]),
+      self::STATE_UNLOCKED => $this->t('Unlocked [@total]', ['@total' => $this->getTotal(NULL, self::STATE_UNLOCKED, $this->sourceEntityTypeId)]),
+    ];
+    // Add draft to state options.
+    if (!$this->webform || $this->webform->getSetting('draft') != WebformInterface::DRAFT_NONE) {
+      $state_options += [
+        self::STATE_COMPLETED => $this->t('Completed [@total]', ['@total' => $this->getTotal(NULL, self::STATE_COMPLETED, $this->sourceEntityTypeId)]),
+        self::STATE_DRAFT => $this->t('Draft [@total]', ['@total' => $this->getTotal(NULL, self::STATE_DRAFT, $this->sourceEntityTypeId)]),
+      ];
+    }
+
+    // Source entity options.
+    if ($this->webform && !$this->sourceEntity) {
+      // < 100 source entities a select menuwill be displayed.
+      // > 100 source entities an autocomplete input will be displayed.
+      $source_entity_total = $this->storage->getSourceEntitiesTotal($this->webform);
+      if ($source_entity_total < 100) {
+        $source_entity_options = $this->storage->getSourceEntitiesAsOptions($this->webform);
+        $source_entity_default_value = $this->sourceEntityTypeId;
+      }
+      elseif ($this->sourceEntityTypeId && strpos($this->sourceEntityTypeId, ':') !== FALSE) {
+        $source_entity_options = $this->webform;
+        try {
+          list($source_entity_type, $source_entity_id) = explode(':', $this->sourceEntityTypeId);
+          $source_entity = $this->entityTypeManager->getStorage($source_entity_type)->load($source_entity_id);
+          $source_entity_default_value = $source_entity->label() . " ($source_entity_type:$source_entity_id)";
+        }
+        catch (\Exception $exception) {
+          $source_entity_default_value = '';
+        }
+      }
+      else {
+        $source_entity_options = $this->webform;
+        $source_entity_default_value = '';
+      }
+    }
+    else {
+      $source_entity_options = NULL;
+      $source_entity_default_value = '';
+    }
+
+    return \Drupal::formBuilder()->getForm('\Drupal\webform\Form\WebformSubmissionFilterForm', $this->keys, $this->state, $state_options, $source_entity_default_value, $source_entity_options);
+  }
+
   /**
    * Build the customize button.
    *
@@ -340,7 +691,7 @@ public function render() {
    */
   protected function buildCustomizeButton() {
     $route_name = $this->requestHandler->getRouteName($this->webform, $this->sourceEntity, 'webform.results_submissions.custom');
-    $route_parameters = $this->requestHandler->getRouteParameters($this->webform, $this->sourceEntity) + ['webform' => $this->webform->id()];
+    $route_parameters = $this->requestHandler->getRouteParameters($this->webform, $this->sourceEntity, $route_name) + ['webform' => $this->webform->id()];
     return [
       '#type' => 'link',
       '#title' => $this->t('Customize'),
@@ -349,6 +700,26 @@ protected function buildCustomizeButton() {
     ];
   }
 
+  /**
+   * Build information summary.
+   *
+   * @return array
+   *   A render array representing the information summary.
+   */
+  protected function buildInfo() {
+    if ($this->account && $this->state == self::STATE_DRAFT) {
+      $info = $this->formatPlural($this->total, '@total draft', '@total drafts', ['@total' => $this->total]);
+    }
+    else {
+      $info = $this->formatPlural($this->total, '@total submission', '@total submissions', ['@total' => $this->total]);
+    }
+    return [
+      '#markup' => $info,
+      '#prefix' => '<div>',
+      '#suffix' => '</div>',
+    ];
+  }
+
   /****************************************************************************/
   // Header functions.
   /****************************************************************************/
@@ -413,8 +784,8 @@ protected function buildHeaderColumn(array $column) {
       case 'sticky':
       case 'locked':
         return [
-          'data' => new FormattableMarkup('<span class="webform-icon webform-icon-@name webform-icon-@name--link"></span>', ['@name' => $name]),
-          'class' => ['webform-results__icon'],
+          'data' => new FormattableMarkup('<span class="webform-icon webform-icon-@name webform-icon-@name--link"></span><span class="visually-hidden">@title</span> ', ['@name' => $name, '@title' => $title]),
+          'class' => ['webform-results-table__icon'],
           'field' => $name,
           'specifier' => $name,
         ];
@@ -468,6 +839,8 @@ public function buildRow(EntityInterface $entity) {
    *   Throw exception if table row column is not found.
    */
   public function buildRowColumn(array $column, EntityInterface $entity) {
+    /** @var $entity \Drupal\webform\WebformSubmissionInterface */
+
     $is_raw = ($column['format'] == 'raw');
     $name = $column['name'];
 
@@ -475,14 +848,14 @@ public function buildRowColumn(array $column, EntityInterface $entity) {
       case 'created':
       case 'completed':
       case 'changed':
-        return ($is_raw) ? $entity->created->value : $entity->created->value ? \Drupal::service('date.formatter')->format($entity->created->value) : '';
+        return ($is_raw) ? $entity->{$name}->value : $entity->{$name}->value ? \Drupal::service('date.formatter')->format($entity->{$name}->value) : '';
 
       case 'entity':
         $source_entity = $entity->getSourceEntity();
         if (!$source_entity) {
           return '';
         }
-        return ($is_raw) ? $source_entity->getEntityTypeId . ':' . $source_entity->id() : ($source_entity->hasLinkTemplate('canonical') ? $source_entity->toLink() : '');
+        return ($is_raw) ? $source_entity->getEntityTypeId . ':' . $source_entity->id() : ($source_entity->hasLinkTemplate('canonical') ? $source_entity->toLink() : $source_entity->label());
 
       case 'langcode':
         $langcode = $entity->langcode->value;
@@ -500,14 +873,16 @@ public function buildRowColumn(array $column, EntityInterface $entity) {
       case 'notes':
         $notes_url = $this->ensureDestination($this->requestHandler->getUrl($entity, $entity->getSourceEntity(), 'webform_submission.notes_form'));
         $state = $entity->get('notes')->value ? 'on' : 'off';
+        $t_args = ['@label' => $entity->label()];
+        $label = $entity->get('notes')->value ? $this->t('Edit @label notes', $t_args) : $this->t('Add notes to @label', $t_args);
         return [
           'data' => [
             '#type' => 'link',
-            '#title' => new FormattableMarkup('<span class="webform-icon webform-icon-notes webform-icon-notes--@state"></span>', ['@state' => $state]),
+            '#title' => new FormattableMarkup('<span class="webform-icon webform-icon-notes webform-icon-notes--@state"></span><span class="visually-hidden">@label</span>', ['@state' => $state, '@label' => $label]),
             '#url' => $notes_url,
-            '#attributes' => WebformDialogHelper::getOffCanvasDialogAttributes(WebformDialogHelper::DIALOG_NARROW),
+            '#attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW),
           ],
-          'class' => ['webform-results__icon'],
+          'class' => ['webform-results-table__icon'],
         ];
 
       case 'operations':
@@ -521,62 +896,66 @@ public function buildRowColumn(array $column, EntityInterface $entity) {
 
       case 'serial':
       case 'label':
-        // Note: Using source entity associate with the submission and not
-        // the current webform.
+        // Note: Use the submission's token URL which points to the
+        // submission's source URL with a secure token.
+        // @see \Drupal\webform\Entity\WebformSubmission::getTokenUrl
         if ($entity->isDraft()) {
-          if ($entity->getSourceEntity()  && $entity->getSourceEntity()->hasLinkTemplate('canonical')) {
-            $link_url = $entity->getSourceEntity()->toUrl('canonical', ['query' => ['token' => $entity->getToken()]]);
-          }
-          else {
-            $link_url = $this->webform->toUrl('canonical', ['query' => ['token' => $entity->getToken()]]);
-          }
+          $link_url = $entity->getTokenUrl();
         }
         else {
           $link_url = $this->requestHandler->getUrl($entity, $entity->getSourceEntity(), $this->getSubmissionRouteName());
         }
         if ($name == 'serial') {
-          $link_text = $entity->serial() . ($entity->isDraft() ? ' (' . $this->t('draft') . ')' : '');
+          $link_text = $entity->serial();
         }
         else {
           $link_text = $entity->label();
         }
-        return Link::fromTextAndUrl($link_text, $link_url);
+        $link = Link::fromTextAndUrl($link_text, $link_url)->toRenderable();
+        if ($name == 'serial') {
+          $link['#attributes']['title'] = $entity->label();
+          $link['#attributes']['aria-label'] = $entity->label();
+        }
+        if ($entity->isDraft()) {
+          $link['#suffix'] = ' (' . $this->t('draft') . ')';
+        }
+        return ['data' => $link];
 
       case 'in_draft':
         return ($entity->isDraft()) ? $this->t('Yes') : $this->t('No');
 
       case 'sticky':
+        // @see \Drupal\webform\Controller\WebformSubmissionController::sticky
         $route_name = 'entity.webform_submission.sticky_toggle';
         $route_parameters = ['webform' => $entity->getWebform()->id(), 'webform_submission' => $entity->id()];
-        $state = $entity->isSticky() ? 'on' : 'off';
         return [
           'data' => [
             '#type' => 'link',
-            '#title' => new FormattableMarkup('<span class="webform-icon webform-icon-sticky webform-icon-sticky--@state"></span>', ['@state' => $state]),
+            '#title' => WebformSubmissionController::buildSticky($entity),
             '#url' => Url::fromRoute($route_name, $route_parameters),
             '#attributes' => [
               'id' => 'webform-submission-' . $entity->id() . '-sticky',
               'class' => ['use-ajax'],
             ],
           ],
-          'class' => ['webform-results__icon'],
+          'class' => ['webform-results-table__icon'],
         ];
 
       case 'locked':
+        // @see \Drupal\webform\Controller\WebformSubmissionController::locked
         $route_name = 'entity.webform_submission.locked_toggle';
         $route_parameters = ['webform' => $entity->getWebform()->id(), 'webform_submission' => $entity->id()];
-        $state = $entity->isLocked() ? 'on' : 'off';
         return [
           'data' => [
             '#type' => 'link',
-            '#title' => new FormattableMarkup('<span class="webform-icon webform-icon-lock webform-icon-locked--@state"></span>', ['@state' => $state]),
+            '#title' => WebformSubmissionController::buildLocked($entity),
             '#url' => Url::fromRoute($route_name, $route_parameters),
             '#attributes' => [
               'id' => 'webform-submission-' . $entity->id() . '-locked',
               'class' => ['use-ajax'],
             ],
           ],
-          'class' => ['webform-results__icon'],
+          'class' => ['webform-results-table__icon'],
         ];
 
       case 'uid':
@@ -613,9 +992,9 @@ public function buildRowColumn(array $column, EntityInterface $entity) {
    */
   public function buildOperations(EntityInterface $entity) {
     return parent::buildOperations($entity) + [
-        '#prefix' => '<div class="webform-dropbutton">',
-        '#suffix' => '</div>',
-      ];
+      '#prefix' => '<div class="webform-dropbutton">',
+      '#suffix' => '</div>',
+    ];
   }
 
   /**
@@ -627,59 +1006,99 @@ public function getDefaultOperations(EntityInterface $entity) {
 
     $operations = [];
 
-    if ($entity->access('update')) {
-      $operations['edit'] = [
-        'title' => $this->t('Edit'),
-        'weight' => 10,
-        'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.edit_form'),
-      ];
-    }
+    if ($this->account) {
+      if ($entity->access('update')) {
+        $operations['edit'] = [
+          'title' => $this->t('Edit'),
+          'weight' => 10,
+          'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform.user.submission.edit'),
+        ];
+      }
 
-    if ($entity->access('view')) {
-      $operations['view'] = [
-        'title' => $this->t('View'),
-        'weight' => 20,
-        'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.canonical'),
-      ];
-    }
+      if ($entity->access('view')) {
+        $operations['view'] = [
+          'title' => $this->t('View'),
+          'weight' => 20,
+          'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform.user.submission'),
+        ];
+      }
 
-    if ($entity->access('notes')) {
-      $operations['notes'] = [
-        'title' => $this->t('Notes'),
-        'weight' => 21,
-        'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.notes_form'),
-      ];
-    }
+      if ($entity->access('create') && $webform->getSetting('submission_user_duplicate')) {
+        $operations['duplicate'] = [
+          'title' => $this->t('Duplicate'),
+          'weight' => 23,
+          'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform.user.submission.duplicate'),
+        ];
+      }
 
-    if ($webform->access('submission_update_any') && $webform->hasMessageHandler()) {
-      $operations['resend'] = [
-        'title' => $this->t('Resend'),
-        'weight' => 22,
-        'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.resend_form'),
-      ];
-    }
-    if ($webform->access('submission_update_any')) {
-      $operations['duplicate'] = [
-        'title' => $this->t('Duplicate'),
-        'weight' => 23,
-        'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.duplicate_form'),
-      ];
+      if ($entity->access('delete')) {
+        $operations['delete'] = [
+          'title' => $this->t('Delete'),
+          'weight' => 100,
+          'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform.user.submission.delete'),
+          'attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW),
+        ];
+      }
     }
+    else {
+      if ($entity->access('update')) {
+        $operations['edit'] = [
+          'title' => $this->t('Edit'),
+          'weight' => 10,
+          'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.edit_form'),
+        ];
+      }
 
-    if ($entity->access('delete')) {
-      $operations['delete'] = [
-        'title' => $this->t('Delete'),
-        'weight' => 100,
-        'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.delete_form'),
-      ];
-    }
+      if ($entity->access('view')) {
+        $operations['view'] = [
+          'title' => $this->t('View'),
+          'weight' => 20,
+          'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.canonical'),
+        ];
+      }
 
-    if ($entity->access('view_any') && \Drupal::currentUser()->hasPermission('access webform submission log')) {
-      $operations['log'] = [
-        'title' => $this->t('Log'),
-        'weight' => 100,
-        'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.log'),
-      ];
+      if ($entity->access('notes')) {
+        $operations['notes'] = [
+          'title' => $this->t('Notes'),
+          'weight' => 21,
+          'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.notes_form'),
+        ];
+      }
+
+      if ($webform->access('submission_update_any') && $webform->hasMessageHandler()) {
+        $operations['resend'] = [
+          'title' => $this->t('Resend'),
+          'weight' => 22,
+          'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.resend_form'),
+        ];
+      }
+      if ($webform->access('submission_update_any')) {
+        $operations['duplicate'] = [
+          'title' => $this->t('Duplicate'),
+          'weight' => 23,
+          'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.duplicate_form'),
+        ];
+      }
+
+      if ($entity->access('delete')) {
+        $operations['delete'] = [
+          'title' => $this->t('Delete'),
+          'weight' => 100,
+          'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.delete_form'),
+          'attributes' => WebformDialogHelper::getModalDialogAttributes(WebformDialogHelper::DIALOG_NARROW),
+        ];
+      }
+
+      if ($entity->access('view_any')
+        && $this->currentUser->hasPermission('access webform submission log')
+        && $webform->hasSubmissionLog()
+        && $this->moduleHandler->moduleExists('webform_submission_log')) {
+        $operations['log'] = [
+          'title' => $this->t('Log'),
+          'weight' => 100,
+          'url' => $this->requestHandler->getUrl($entity, $this->sourceEntity, 'webform_submission.log'),
+        ];
+      }
     }
 
     // Add destination to all operation links.
@@ -702,7 +1121,7 @@ public function getDefaultOperations(EntityInterface $entity) {
    *   or 'webform_submission.canonical.
    */
   protected function getSubmissionRouteName() {
-    return (strpos(\Drupal::routeMatch()->getRouteName(), 'webform.user.submissions') !== FALSE) ? 'webform.user.submission' : 'webform_submission.canonical';
+    return (strpos($this->routeMatch->getRouteName(), 'webform.user.submissions') !== FALSE) ? 'webform.user.submission' : 'webform_submission.' . $this->linkType;
   }
 
   /**
@@ -732,6 +1151,112 @@ protected function getRouteParameters(WebformSubmissionInterface $webform_submis
     return $route_parameters;
   }
 
+  /****************************************************************************/
+  // Submission views functions.
+  /****************************************************************************/
+
+  /**
+   * Get the submission view type for the current route.
+   *
+   * @return string
+   *   The submission view type for the current route.
+   */
+  protected function getSubmissionViewType() {
+    if (!$this->webform) {
+      return 'global';
+    }
+    elseif ($this->sourceEntity && $this->sourceEntity->getEntityTypeId() === 'node') {
+      return 'node';
+    }
+    else {
+      return 'webform';
+    }
+  }
+
+  /**
+   * Determine if the submission view(s) replaced the default results table.
+   *
+   * @return bool
+   *   TRUE if the submission view(s) replaced the default results table.
+   */
+  protected function isSubmissionViewResultsReplaced() {
+    $type = $this->getSubmissionViewType();
+
+    $replace_routes = [];
+
+    $default_replace = $this->configFactory->get('webform.settings')->get('settings.default_submission_views_replace') ?: [];
+    if (isset($default_replace[$type . '_routes'])) {
+      $replace_routes = array_merge($replace_routes, $default_replace[$type . '_routes']);
+    }
+
+    if ($this->webform) {
+      $webform_replace = $this->webform->getSetting('submission_views_replace') ?: [];
+      if (isset($webform_replace[$type . '_routes'])) {
+        $replace_routes = array_merge($replace_routes, $webform_replace[$type . '_routes']);
+      }
+    }
+
+    $route_name = $this->routeMatch->getRouteName();
+    return ($replace_routes && in_array($route_name, $replace_routes)) ? TRUE : FALSE;
+  }
+
+  /**
+   * Get all submission views applicable.
+   *
+   * @return array
+   *   An associative array of all submission views.
+   */
+  protected function getSubmissionViewsConfig() {
+    // Merge webform submission views with global submission views.
+    $submission_views = [];
+    if ($this->webform) {
+      $submission_views += $this->webform->getSetting('submission_views') ?: [];
+    }
+    $submission_views += $this->configFactory->get('webform.settings')->get('settings.default_submission_views') ?: [];
+    return $submission_views;
+  }
+
+  /**
+   * Get submission views applicable for the current route and user.
+   *
+   * @return array
+   *   An associative array of submission views applicable for the
+   *   current route and user.
+   */
+  protected function getSubmissionViews() {
+    if (!$this->moduleHandler()->moduleExists('views')) {
+      return [];
+    }
+
+    $type = $this->getSubmissionViewType();
+    $route_name = $this->routeMatch->getRouteName();
+
+    $submission_views = $this->getSubmissionViewsConfig();
+    foreach ($submission_views as $name => $submission_view) {
+      $submission_view += [
+        'global_routes' => [],
+        'webform_routes' => [],
+        'node_routes' => [],
+      ];
+
+      // Check global, webform, or node routes.
+      $routes = $submission_view[$type . '_routes'];
+      if (empty($routes) || !in_array($route_name, $routes)) {
+        unset($submission_views[$name]);
+        continue;
+      }
+
+      list($view_name, $view_display_id) = explode(':', $submission_view['view']);
+      $view = Views::getView($view_name);
+      if (!$view || !$view->access($view_display_id)) {
+        unset($submission_views[$name]);
+        continue;
+      }
+    }
+
+    return $submission_views;
+  }
+
   /****************************************************************************/
   // Query functions.
   /****************************************************************************/
@@ -740,7 +1265,7 @@ protected function getRouteParameters(WebformSubmissionInterface $webform_submis
    * {@inheritdoc}
    */
   protected function getEntityIds() {
-    $query = $this->getQuery($this->keys, $this->state);
+    $query = $this->getQuery($this->keys, $this->state, $this->sourceEntityTypeId);
     $query->pager($this->limit);
 
     $header = $this->buildHeader();
@@ -748,14 +1273,15 @@ protected function getEntityIds() {
     $direction = tablesort_get_sort($header);
 
     // If query is order(ed) by 'element__*' we need to build a custom table
-    // sort using hook_query_alter().
-    // @see: webform_query_alter()
+    // sort using hook_query_TAG_alter().
+    // @see webform_query_webform_submission_list_builder_alter()
     if ($order && strpos($order['sql'], 'element__') === 0) {
       $name = $order['sql'];
       $column = $this->columns[$name];
-      $query->addMetaData('webform_submission_element_name', $column['key']);
-      $query->addMetaData('webform_submission_element_property_name', $column['property_name']);
-      $query->addMetaData('webform_submission_element_direction', $direction);
+      $query->addTag('webform_submission_list_builder')
+        ->addMetaData('webform_submission_element_name', $column['key'])
+        ->addMetaData('webform_submission_element_property_name', $column['property_name'])
+        ->addMetaData('webform_submission_element_direction', $direction);
       $result = $query->execute();
       // Must manually initialize the pager because the DISTINCT clause in the
       // query is breaking the row counting.
@@ -764,7 +1290,7 @@ protected function getEntityIds() {
       return $result;
     }
     else {
-      $order = \Drupal::request()->query->get('order', '');
+      $order = $this->request->query->get('order', '');
       if ($order) {
         $query->tableSort($header);
       }
@@ -792,12 +1318,14 @@ protected function getEntityIds() {
    *   (optional) Search key.
    * @param string $state
    *   (optional) Submission state.
+   * @param string $source_entity
+   *   (optional) Source entity (type:id).
    *
    * @return int
    *   The total number of submissions.
    */
-  protected function getTotal($keys = '', $state = '') {
-    return $this->getQuery($keys, $state)
+  protected function getTotal($keys = '', $state = '', $source_entity = '') {
+    return $this->getQuery($keys, $state, $source_entity)
       ->count()
       ->execute();
   }
@@ -809,11 +1337,13 @@ protected function getTotal($keys = '', $state = '') {
    *   (optional) Search key.
    * @param string $state
    *   (optional) Submission state.
+   * @param string $source_entity
+   *   (optional) Source entity (type:id).
    *
    * @return \Drupal\Core\Entity\Query\QueryInterface
    *   An entity query.
    */
-  protected function getQuery($keys = '', $state = '') {
+  protected function getQuery($keys = '', $state = '', $source_entity = '') {
     /** @var \Drupal\webform\WebformSubmissionStorageInterface $submission_storage */
     $submission_storage = $this->getStorage();
     $query = $submission_storage->getQuery();
@@ -828,14 +1358,17 @@ protected function getQuery($keys = '', $state = '') {
       $submission_storage->addQueryConditions($sub_query, $this->webform);
 
       // Search UUID and Notes.
+      $or_condition = $query->orConditionGroup();
+      $or_condition->condition('notes', '%' . $keys . '%', 'LIKE');
+      // Only search UUID if keys is alphanumeric with dashes.
+      // @see Issue #2978420: Error SQL with accent mark submissions filter.
+      if (preg_match('/^[0-9a-z-]+$/', $keys)) {
+        $or_condition->condition('uuid', $keys);
+      }
       $query->condition(
         $query->orConditionGroup()
           ->condition('sid', $sub_query, 'IN')
-          ->condition(
-            $query->orConditionGroup()
-              ->condition('uuid', $keys)
-              ->condition('notes', '%' . $keys . '%', 'LIKE')
-          )
+          ->condition($or_condition)
       );
     }
 
@@ -866,25 +1399,19 @@ protected function getQuery($keys = '', $state = '') {
         break;
     }
 
-    return $query;
-  }
+    // Filter by source entity.
+    if ($source_entity && strpos($source_entity, ':') !== FALSE) {
+      list($entity_type, $entity_id) = explode(':', $source_entity);
+      $query->condition('entity_type', $entity_type);
+      $query->condition('entity_id', $entity_id);
+    }
 
-  /****************************************************************************/
-  // Backport of Drupal 8.5.x+ methods.
-  // @todo Remove once only Drupal 8.5.x+ is supported.
-  /****************************************************************************/
+    // Filter by draft. (Only applies to user submissions and drafts)
+    if (isset($this->draft)) {
+      $query->condition('in_draft', $this->draft);
+    }
 
-  /**
-   * Ensures that a destination is present on the given URL.
-   *
-   * @param \Drupal\Core\Url $url
-   *   The URL object to which the destination should be added.
-   *
-   * @return \Drupal\Core\Url
-   *   The updated URL object.
-   */
-  protected function ensureDestination(Url $url) {
-    return $url->mergeOptions(['query' => \Drupal::destination()->getAsArray()]);
+    return $query;
   }
 
 }
diff --git a/web/modules/webform/src/WebformSubmissionNotesForm.php b/web/modules/webform/src/WebformSubmissionNotesForm.php
index 206862224bf58ca3f754d525f9c27b4b791dda68..748dbfc3e8ad961fa0dcfbbc1e935afdac201653 100644
--- a/web/modules/webform/src/WebformSubmissionNotesForm.php
+++ b/web/modules/webform/src/WebformSubmissionNotesForm.php
@@ -29,8 +29,8 @@ class WebformSubmissionNotesForm extends ContentEntityForm {
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
    *   The entity manager.
-   * @param Drupal\webform\WebformRequestInterface $webform_request
-   *   The webform request.
+   * @param \Drupal\webform\WebformRequestInterface $webform_request
+   *   The webform request handler.
    * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
    *   The entity type bundle service.
    * @param \Drupal\Component\Datetime\TimeInterface $time
@@ -64,12 +64,12 @@ public function form(array $form, FormStateInterface $form_state) {
     list($webform_submission, $source_entity) = $this->requestHandler->getWebformSubmissionEntities();
 
     $form['navigation'] = [
-      '#theme' => 'webform_submission_navigation',
+      '#type' => 'webform_submission_navigation',
       '#webform_submission' => $webform_submission,
       '#access' => $this->isDialog() ? FALSE : TRUE,
     ];
     $form['information'] = [
-      '#theme' => 'webform_submission_information',
+      '#type' => 'webform_submission_information',
       '#webform_submission' => $webform_submission,
       '#source_entity' => $source_entity,
       '#access' => $this->isDialog() ? FALSE : TRUE,
@@ -107,6 +107,7 @@ public function form(array $form, FormStateInterface $form_state) {
       ],
       '#required' => TRUE,
       '#default_value' => $webform_submission->getOwner(),
+      '#access' => $this->currentUser()->hasPermission('administer users'),
     ];
 
     $form['#attached']['library'][] = 'webform/webform.admin';
@@ -136,7 +137,7 @@ protected function actions(array $form, FormStateInterface $form_state) {
    */
   public function save(array $form, FormStateInterface $form_state) {
     parent::save($form, $form_state);
-    drupal_set_message($this->t('Submission @sid notes saved.', ['@sid' => '#' . $this->entity->id()]));
+    $this->messenger()->addStatus($this->t('Submission @sid notes saved.', ['@sid' => '#' . $this->entity->id()]));
   }
 
   /**
diff --git a/web/modules/webform/src/WebformSubmissionStorage.php b/web/modules/webform/src/WebformSubmissionStorage.php
index 498d43c9ebb3c29be299230863f898dee80c0b7a..71687e7450f13339c2e87803db94ec2cb7e71698 100644
--- a/web/modules/webform/src/WebformSubmissionStorage.php
+++ b/web/modules/webform/src/WebformSubmissionStorage.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\webform;
 
+use Drupal\Component\Datetime\TimeInterface;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Database\Query\AlterableInterface;
@@ -19,6 +20,7 @@
 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
 use Drupal\user\Entity\User;
 use Drupal\user\UserInterface;
+use Drupal\webform\Plugin\WebformElement\WebformManagedFileBase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -33,6 +35,13 @@ class WebformSubmissionStorage extends SqlContentEntityStorage implements Webfor
    */
   protected $elementDataSchema = [];
 
+  /**
+   * The time service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
   /**
    * The current user.
    *
@@ -40,13 +49,24 @@ class WebformSubmissionStorage extends SqlContentEntityStorage implements Webfor
    */
   protected $currentUser;
 
+  /**
+   * Webform access rules manager service.
+   *
+   * @var \Drupal\webform\WebformAccessRulesManagerInterface
+   */
+  protected $accessRulesManager;
+
   /**
    * WebformSubmissionStorage constructor.
+   *
+   * @todo Webform 8.x-6.x: Move $time before $access_rules_manager.
    */
-  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, AccountProxyInterface $current_user) {
+  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, AccountProxyInterface $current_user, WebformAccessRulesManagerInterface $access_rules_manager, TimeInterface $time = NULL) {
     parent::__construct($entity_type, $database, $entity_manager, $cache, $language_manager);
 
     $this->currentUser = $current_user;
+    $this->accessRulesManager = $access_rules_manager;
+    $this->time = $time;
   }
 
   /**
@@ -59,7 +79,9 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
       $container->get('entity.manager'),
       $container->get('cache.entity'),
       $container->get('language_manager'),
-      $container->get('current_user')
+      $container->get('current_user'),
+      $container->get('webform.access_rules_manager'),
+      $container->get('datetime.time')
     );
   }
 
@@ -95,7 +117,7 @@ public function getFieldDefinitions() {
    * {@inheritdoc}
    */
   public function checkFieldDefinitionAccess(WebformInterface $webform, array $definitions) {
-    if (!$webform->access('submission_upates_any')) {
+    if (!$webform->access('submission_update_any')) {
       unset($definitions['token']);
     }
     return $definitions;
@@ -117,9 +139,9 @@ protected function doCreate(array $values) {
   /**
    * {@inheritdoc}
    */
-  public function loadMultiple(array $ids = NULL) {
+  protected function doLoadMultiple(array $ids = NULL) {
     /** @var \Drupal\webform\WebformSubmissionInterface[] $webform_submissions */
-    $webform_submissions = parent::loadMultiple($ids);
+    $webform_submissions = parent::doLoadMultiple($ids);
     $this->loadData($webform_submissions);
     return $webform_submissions;
   }
@@ -191,8 +213,10 @@ protected function buildPropertyQuery(QueryInterface $entity_query, array $value
     // Add account query wheneven filter by uid.
     if (isset($values['uid'])) {
       $account = User::load($values['uid']);
-      $this->addQueryConditions($entity_query, NULL, NULL, $account);
-      unset($values['uid']);
+      if ($account instanceof UserInterface) {
+        $this->addQueryConditions($entity_query, NULL, NULL, $account);
+        unset($values['uid']);
+      }
     }
 
     parent::buildPropertyQuery($entity_query, $values);
@@ -203,6 +227,7 @@ protected function buildPropertyQuery(QueryInterface $entity_query, array $value
    */
   public function deleteAll(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, $limit = NULL, $max_sid = NULL) {
     $query = $this->getQuery();
+    $query->accessCheck(FALSE);
     $this->addQueryConditions($query, $webform, $source_entity, NULL);
     if ($max_sid) {
       $query->condition('sid', $max_sid, '<=');
@@ -228,6 +253,7 @@ public function getTotal(WebformInterface $webform = NULL, EntityInterface $sour
     ];
 
     $query = $this->getQuery();
+    $query->accessCheck(FALSE);
     $this->addQueryConditions($query, $webform, $source_entity, $account, $options);
 
     // Issue: Query count method is not working for SQL Lite.
@@ -241,6 +267,7 @@ public function getTotal(WebformInterface $webform = NULL, EntityInterface $sour
    */
   public function getMaxSubmissionId(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL) {
     $query = $this->getQuery();
+    $query->accessCheck(FALSE);
     $this->addQueryConditions($query, $webform, $source_entity, $account);
     $query->sort('sid', 'DESC');
     $query->range(0, 1);
@@ -262,6 +289,69 @@ public function hasSubmissionValue(WebformInterface $webform, $element_key) {
     return $result->fetchAssoc() ? TRUE : FALSE;
   }
 
+  /****************************************************************************/
+  // Source entity methods.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourceEntitiesTotal(WebformInterface $webform) {
+    $query = $this->database->select('webform_submission', 's')
+      ->fields('s', ['entity_type', 'entity_id'])
+      ->condition('webform_id', $webform->id())
+      ->condition('entity_type', '', '<>')
+      ->isNotNull('entity_type')
+      ->condition('entity_id', '', '<>')
+      ->isNotNull('entity_id')
+      ->distinct();
+    return (int) $query->countQuery()->execute()->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourceEntities(WebformInterface $webform) {
+    /** @var \Drupal\Core\Database\StatementInterface $result */
+    $result = $this->database->select('webform_submission', 's')
+      ->fields('s', ['entity_type', 'entity_id'])
+      ->condition('webform_id', $webform->id())
+      ->condition('entity_type', '', '<>')
+      ->isNotNull('entity_type')
+      ->condition('entity_id', '', '<>')
+      ->isNotNull('entity_id')
+      ->distinct()
+      ->execute();
+    $source_entities = [];
+    while ($record = $result->fetchAssoc()) {
+      $source_entities[$record['entity_type']][$record['entity_id']] = $record['entity_id'];
+    }
+    return $source_entities;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourceEntitiesAsOptions(WebformInterface $webform) {
+    $options = [];
+    $source_entities = $this->getSourceEntities($webform);
+    foreach ($source_entities as $entity_type => $entity_ids) {
+      $optgroup = (string) $this->entityManager->getDefinition($entity_type)->getCollectionLabel();
+      $entities = $this->entityManager->getStorage($entity_type)->loadMultiple($entity_ids);
+      foreach ($entities as $entity_id => $entity) {
+        if ($entity instanceof TranslatableInterface && $entity->hasTranslation($this->languageManager->getCurrentLanguage()->getId())) {
+          $entity = $entity->getTranslation($this->languageManager->getCurrentLanguage()->getId());
+        }
+
+        $option_value = "$entity_type:$entity_id";
+        $option_text = $entity->label();
+        $options[$optgroup][$option_value] = $option_text;
+      }
+      asort($options[$optgroup]);
+    }
+    return (count($options) === 1) ? reset($options) : $options;
+  }
+
   /****************************************************************************/
   // Query methods.
   /****************************************************************************/
@@ -439,27 +529,6 @@ protected function getSiblingSubmission(WebformSubmissionInterface $webform_subm
   // WebformSubmissionEntityList methods.
   /****************************************************************************/
 
-  /**
-   * Get specified columns in specified order.
-   *
-   * @param array $column_names
-   *   An associative array of column names.
-   * @param array $columns
-   *   An associative array containing all available columns.
-   *
-   * @return array
-   *   An associative array containing all specified columns.
-   */
-  protected function filterColumns(array $column_names, array $columns) {
-    $filtered_columns = [];
-    foreach ($column_names as $column_name) {
-      if (isset($columns[$column_name])) {
-        $filtered_columns[$column_name] = $columns[$column_name];
-      }
-    }
-    return $filtered_columns;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -491,35 +560,88 @@ public function getUserColumns(WebformInterface $webform = NULL, EntityInterface
     $column_names = ($webform) ? $webform->getSetting('submission_user_columns', []) : [];
     $column_names = $column_names ?: $this->getUserDefaultColumnNames($webform, $source_entity, $account, $include_elements);
     $columns = $this->getColumns($webform, $source_entity, $account, $include_elements);
+    unset($columns['sid']);
     return $this->filterColumns($column_names, $columns);
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getUserDefaultColumnNames(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE) {
-    return ['serial', 'created', 'remote_addr'];
+  public function getDefaultColumns(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE) {
+    $columns = $this->getColumns($webform, $source_entity, $account, $include_elements);
+
+    // Unset columns.
+    unset(
+      // Admin columns.
+      $columns['sid'],
+      $columns['label'],
+      $columns['uuid'],
+      $columns['in_draft'],
+      $columns['completed'],
+      $columns['changed']
+    );
+
+    // Hide certain unnecessary columns, that have default set to FALSE.
+    foreach ($columns as $column_name => $column) {
+      if (isset($column['default']) && $column['default'] === FALSE) {
+        unset($columns[$column_name]);
+      }
+    }
+
+    return $columns;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getDefaultColumnNames(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE) {
-    $columns = $this->getDefaultColumns($webform, $source_entity, $account, $include_elements);
-    return array_keys($columns);
+  public function getSubmissionsColumns() {
+    $columns = $this->getColumns(NULL, NULL, NULL, FALSE);
+
+    // Unset columns.
+    // Note: 'serial' is displayed instead of 'sid'.
+    unset(
+      // Admin columns.
+      $columns['serial'],
+      $columns['label'],
+      $columns['uuid'],
+      $columns['in_draft'],
+      $columns['completed'],
+      $columns['changed'],
+      // User columns.
+      $columns['sticky'],
+      $columns['locked'],
+      $columns['notes'],
+      $columns['uid']
+    );
+    return $columns;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getDefaultColumns(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE) {
-    $columns = $this->getColumns($webform, $source_entity, $account, $include_elements);
-    // Hide certain unnecessary columns, that have default set to FALSE.
-    foreach ($columns as $column_name => $column) {
-      if (isset($column['default']) && $column['default'] === FALSE) {
-        unset($columns[$column_name]);
-      }
-    }
+  public function getUsersSubmissionsColumns() {
+    $columns = $this->getColumns(NULL, NULL, NULL, FALSE);
+    // Unset columns.
+    // Note: Displaying 'label' instead of 'serial' or 'sid'.
+    unset(
+      // Admin columns.
+      $columns['sid'],
+      $columns['serial'],
+      $columns['uuid'],
+      $columns['in_draft'],
+      $columns['completed'],
+      $columns['changed'],
+      // User columns.
+      $columns['sticky'],
+      $columns['locked'],
+      $columns['notes'],
+      $columns['uid'],
+      // References columns.
+      $columns['webform_id'],
+      $columns['entity'],
+      // Operations.
+      $columns['operations']
+    );
     return $columns;
   }
 
@@ -539,26 +661,22 @@ public function getColumns(WebformInterface $webform = NULL, EntityInterface $so
     // Submission ID.
     $columns['sid'] = [
       'title' => $this->t('SID'),
-      'default' => FALSE,
     ];
 
     // Submission label.
     $columns['label'] = [
       'title' => $this->t('Submission title'),
-      'default' => FALSE,
       'sort' => FALSE,
     ];
 
     // UUID.
     $columns['uuid'] = [
       'title' => $this->t('UUID'),
-      'default' => FALSE,
     ];
 
     // Draft.
     $columns['in_draft'] = [
       'title' => $this->t('In draft'),
-      'default' => FALSE,
     ];
 
     if (empty($account)) {
@@ -586,13 +704,11 @@ public function getColumns(WebformInterface $webform = NULL, EntityInterface $so
     // Completed.
     $columns['completed'] = [
       'title' => $this->t('Completed'),
-      'default' => FALSE,
     ];
 
     // Changed.
     $columns['changed'] = [
       'title' => $this->t('Changed'),
-      'default' => FALSE,
     ];
 
     // Source entity.
@@ -622,11 +738,16 @@ public function getColumns(WebformInterface $webform = NULL, EntityInterface $so
       'title' => $this->t('IP address'),
     ];
 
-    // Webform.
+    // Webform and source entity for entity.webform_submission.collection.
+    // @see /admin/structure/webform/submissions/manage
     if (empty($webform) && empty($source_entity)) {
       $columns['webform_id'] = [
         'title' => $this->t('Webform'),
       ];
+      $columns['entity'] = [
+        'title' => $this->t('Submitted to'),
+        'sort' => FALSE,
+      ];
     }
 
     // Webform elements.
@@ -637,17 +758,17 @@ public function getColumns(WebformInterface $webform = NULL, EntityInterface $so
       foreach ($elements as $element) {
         /** @var \Drupal\webform\Plugin\WebformElementInterface $element_plugin */
         $element_plugin = $element_manager->createInstance($element['#type']);
+        // Replace tokens which can be used in an element's #title.
+        $element_plugin->replaceTokens($element, $webform);
         $columns += $element_plugin->getTableColumn($element);
       }
     }
 
     // Operations.
-    if (empty($account)) {
-      $columns['operations'] = [
-        'title' => $this->t('Operations'),
-        'sort' => FALSE,
-      ];
-    }
+    $columns['operations'] = [
+      'title' => $this->t('Operations'),
+      'sort' => FALSE,
+    ];
 
     // Add name and format to all columns.
     foreach ($columns as $name => &$column) {
@@ -658,6 +779,46 @@ public function getColumns(WebformInterface $webform = NULL, EntityInterface $so
     return $columns;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getUserDefaultColumnNames(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE) {
+    return ['serial', 'created', 'remote_addr'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultColumnNames(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE) {
+    $columns = $this->getDefaultColumns($webform, $source_entity, $account, $include_elements);
+    return array_keys($columns);
+  }
+
+  /**
+   * Get specified columns in specified order.
+   *
+   * @param array $column_names
+   *   An associative array of column names.
+   * @param array $columns
+   *   An associative array containing all available columns.
+   *
+   * @return array
+   *   An associative array containing all specified columns.
+   */
+  protected function filterColumns(array $column_names, array $columns) {
+    $filtered_columns = [];
+    foreach ($column_names as $column_name) {
+      if (isset($columns[$column_name])) {
+        $filtered_columns[$column_name] = $columns[$column_name];
+      }
+    }
+    return $filtered_columns;
+  }
+
+  /****************************************************************************/
+  // Custom settings methods.
+  /****************************************************************************/
+
   /**
    * {@inheritdoc}
    */
@@ -772,84 +933,91 @@ protected function doPostSave(EntityInterface $entity, $update) {
     /** @var \Drupal\webform\WebformSubmissionInterface $entity */
     parent::doPostSave($entity, $update);
 
-    // Log transaction.
     $webform = $entity->getWebform();
-    if (!$entity->getWebform()->getSetting('results_disabled')) {
+
+    if ($webform->hasSubmissionLog()) {
+      // Log webform submission events to the 'webform_submission' log.
       $context = [
-        '@id' => $entity->id(),
-        '@form' => $webform->label(),
-        'link' => $entity->toLink($this->t('Edit'), 'edit-form')->toString(),
+        '@title' => $entity->label(),
+        'link' => ($entity->id()) ? $entity->toLink($this->t('Edit'), 'edit-form')->toString() : NULL,
+        'webform_submission' => $entity,
       ];
-      switch ($entity->getState()) {
-        case WebformSubmissionInterface::STATE_DRAFT:
-          \Drupal::logger('webform')->notice('@form: Submission #@id draft saved.', $context);
-          break;
-
-        case WebformSubmissionInterface::STATE_UPDATED:
-          \Drupal::logger('webform')->notice('@form: Submission #@id updated.', $context);
-          break;
-
-        case WebformSubmissionInterface::STATE_COMPLETED:
-          if ($update) {
-            \Drupal::logger('webform')->notice('@form: Submission #@id completed.', $context);
-          }
-          else {
-            \Drupal::logger('webform')->notice('@form: Submission #@id created.', $context);
-          }
-          break;
-      }
-    }
-
-    // Log submission events.
-    if ($entity->getWebform()->hasSubmissionLog()) {
-      $t_args = ['@title' => $entity->label()];
       switch ($entity->getState()) {
         case WebformSubmissionInterface::STATE_DRAFT:
           if ($update) {
-            $operation = 'draft updated';
-            $message = $this->t('@title draft updated.', $t_args);
+            $message = '@title draft updated.';
+            $context['operation'] = 'draft updated';
           }
           else {
-            $operation = 'draft created';
-            $message = $this->t('@title draft created.', $t_args);
+            $message = '@title draft created.';
+            $context['operation'] = 'draft created';
           }
           break;
 
         case WebformSubmissionInterface::STATE_COMPLETED:
           if ($update) {
-            $operation = 'submission completed';
-            $message = $this->t('@title completed using saved draft.', $t_args);
+            $message = '@title completed using saved draft.';
+            $context['operation'] = 'submission completed';
           }
           else {
-            $operation = 'submission created';
-            $message = $this->t('@title created.', $t_args);
+            $message = '@title created.';
+            $context['operation'] = 'submission created';
           }
           break;
 
         case WebformSubmissionInterface::STATE_CONVERTED:
-          $operation = 'submission converted';
-          $message = $this->t('@title converted from anonymous to @user.', $t_args + ['@user' => $entity->getOwner()->label()]);
+          $message = '@title converted from anonymous to @user.';
+          $context['operation'] = 'submission converted';
+          $context['@user'] = $entity->getOwner()->label();
           break;
 
         case WebformSubmissionInterface::STATE_UPDATED:
-          $operation = 'submission updated';
-          $message = $this->t('@title updated.', $t_args);
+          $message = '@title updated.';
+          $context['operation'] = 'submission updated';
           break;
 
         case WebformSubmissionInterface::STATE_UNSAVED:
-          $operation = 'submission submitted';
-          $message = $this->t('@title submitted.', $t_args);
+          $message = '@title submitted.';
+          $context['operation'] = 'submission submitted';
+          break;
+
+        case WebformSubmissionInterface::STATE_LOCKED:
+          $message = '@title locked.';
+          $context['operation'] = 'submission locked';
           break;
 
         default:
           throw new \Exception('Unexpected webform submission state');
       }
+      \Drupal::logger('webform_submission')->notice($message, $context);
+    }
+    elseif (!$webform->getSetting('results_disabled')) {
+      // Log general events to the 'webform'.
+      switch ($entity->getState()) {
+        case WebformSubmissionInterface::STATE_DRAFT:
+          $message = '@title draft saved.';
+          break;
+
+        case WebformSubmissionInterface::STATE_UPDATED:
+          $message = '@title updated.';
+          break;
+
+        case WebformSubmissionInterface::STATE_COMPLETED:
+          $message = ($update) ? '@title completed.' : '@title created.';
+          break;
 
-      $this->log($entity, [
-        'handler_id' => '',
-        'operation' => $operation,
-        'message' => $message,
-      ]);
+        default:
+          $message = NULL;
+          break;
+      }
+      if ($message) {
+        $context = [
+          '@id' => $entity->id(),
+          '@title' => $entity->label(),
+          'link' => ($entity->id()) ? $entity->toLink($this->t('Edit'), 'edit-form')->toString() : NULL,
+        ];
+        \Drupal::logger('webform')->notice($message, $context);
+      }
     }
 
     $this->invokeWebformElements('postSave', $entity, $update);
@@ -898,6 +1066,8 @@ public function delete(array $entities) {
     foreach ($entities as $entity) {
       $this->invokeWebformElements('postDelete', $entity);
       $this->invokeWebformHandlers('postDelete', $entity);
+
+      WebformManagedFileBase::deleteFiles($entity);
     }
 
     // Remove empty webform submission specific file directory
@@ -917,9 +1087,6 @@ public function delete(array $entities) {
       }
     }
 
-    // Delete submission log after all pre and post delete hooks are called.
-    $this->deleteLog($entities);
-
     // Log deleted.
     foreach ($entities as $entity) {
       \Drupal::logger('webform')
@@ -963,6 +1130,7 @@ public function purge($count) {
     $days_to_seconds = 60 * 60 * 24;
 
     $query = $this->entityManager->getStorage('webform')->getQuery();
+    $query->accessCheck(FALSE);
     $query->condition('settings.purge', [self::PURGE_DRAFT, self::PURGE_COMPLETED, self::PURGE_ALL], 'IN');
     $query->condition('settings.purge_days', 0, '>');
     $webforms_to_purge = array_values($query->execute());
@@ -973,7 +1141,11 @@ public function purge($count) {
       $webforms_to_purge = $this->entityManager->getStorage('webform')->loadMultiple($webforms_to_purge);
       foreach ($webforms_to_purge as $webform) {
         $query = $this->getQuery();
-        $query->condition('created', REQUEST_TIME - ($webform->getSetting('purge_days') * $days_to_seconds), '<');
+        // Since results of this query are never displayed to the user and we
+        // actually need to query the entire dataset of webform submissions, we
+        // are disabling access check.
+        $query->accessCheck(FALSE);
+        $query->condition('created', $this->time->getRequestTime() - ($webform->getSetting('purge_days') * $days_to_seconds), '<');
         $query->condition('webform_id', $webform->id());
         switch ($webform->getSetting('purge')) {
           case self::PURGE_DRAFT:
@@ -1153,42 +1325,24 @@ protected function deleteData(array $webform_submissions) {
   /**
    * {@inheritdoc}
    */
-  public function log(WebformSubmissionInterface $webform_submission, array $values = []) {
+  public function log(WebformSubmissionInterface $webform_submission, array $context = []) {
     // Submission ID is required for logging.
-    // @todo Enable logging for submissions not saved to the database.
     if (empty($webform_submission->id())) {
       return;
     }
 
-    $values += [
+    $message = $context['message'];
+    unset($context['message']);
+
+    $context += [
       'uid' => $this->currentUser->id(),
-      'webform_id' => $webform_submission->getWebform()->id(),
-      'sid' => $webform_submission->id() ?: NULL,
+      'webform_submission' => $webform_submission,
       'handler_id' => NULL,
       'data' => [],
-      'timestamp' => time(),
+      'link' => $webform_submission->toLink($this->t('Edit'), 'edit-form')->toString(),
     ];
-    $values['data'] = serialize($values['data']);
-    \Drupal::database()
-      ->insert('webform_submission_log')
-      ->fields($values)
-      ->execute();
-  }
 
-  /**
-   * Delete webform submission events from the 'webform_submission_log' table.
-   *
-   * @param array $webform_submissions
-   *   An array of webform submissions.
-   */
-  protected function deleteLog(array $webform_submissions) {
-    $sids = [];
-    foreach ($webform_submissions as $webform_submission) {
-      $sids[$webform_submission->id()] = $webform_submission->id();
-    }
-    $this->database->delete('webform_submission_log')
-      ->condition('sid', $sids, 'IN')
-      ->execute();
+    \Drupal::logger('webform_submission')->notice($message, $context);
   }
 
   /****************************************************************************/
@@ -1205,6 +1359,11 @@ public function loadDraft(WebformInterface $webform, EntityInterface $source_ent
     ];
 
     $query = $this->getQuery();
+    // Because draft is somewhat different from a complete webform submission,
+    // we allow to bypass access check. Moreover, draft here is enforced to be
+    // authored by the $account user. Thus we hardly open any security breach
+    // here.
+    $query->accessCheck(FALSE);
     $this->addQueryConditions($query, $webform, $source_entity, $account, $options);
 
     // Only load the most recent draft.
@@ -1227,6 +1386,7 @@ public function userLogin(UserInterface $account) {
 
     // Move all anonymous submissions to UID of this account.
     $query = $this->getQuery();
+    $query->accessCheck(FALSE);
     $query->condition('uid', 0);
     $query->condition('sid', $_SESSION['webform_submissions'], 'IN');
     $query->sort('sid');
@@ -1302,7 +1462,7 @@ protected function checkAnonymousSubmissionAccess(WebformSubmissionInterface $we
     if ($this->currentUser->hasPermission('view own webform submission')) {
       return TRUE;
     }
-    elseif ($webform->checkAccessRules('view_own', $this->currentUser)->isAllowed()) {
+    elseif ($this->accessRulesManager->checkWebformSubmissionAccess('view_own', $this->currentUser, $webform_submission)->isAllowed()) {
       return TRUE;
     }
     elseif ($webform->getSetting('form_convert_anonymous')) {
@@ -1320,7 +1480,7 @@ protected function checkAnonymousSubmissionAccess(WebformSubmissionInterface $we
   }
 
   /**
-   * Get anonymous users sumbmission ids.
+   * Get anonymous user's submission ids.
    *
    * @param \Drupal\Core\Session\AccountInterface|null $account
    *   A user account.
@@ -1342,6 +1502,9 @@ protected function getAnonymousSubmissionIds(AccountInterface $account) {
     // Cleanup sids because drafts could have been purged or the webform
     // submission could have been deleted.
     $_SESSION['webform_submissions'] = $this->getQuery()
+      // Disable access check because user having 'sid' in his $_SESSION already
+      // implies he has access to it.
+      ->accessCheck(FALSE)
       ->condition('sid', $_SESSION['webform_submissions'], 'IN')
       ->sort('sid')
       ->execute();
diff --git a/web/modules/webform/src/WebformSubmissionStorageInterface.php b/web/modules/webform/src/WebformSubmissionStorageInterface.php
index aca641574a88220bf57c356c0711d44aed173fd0..b7988e1e66ebcf3d7f06bbd64359325baa89f5b4 100644
--- a/web/modules/webform/src/WebformSubmissionStorageInterface.php
+++ b/web/modules/webform/src/WebformSubmissionStorageInterface.php
@@ -63,7 +63,7 @@ public function getFieldDefinitions();
   /**
    * Check field definition access.
    *
-   * Access checks include...
+   * Access checks include…
    * - Only allowing user who can update any access to the 'token' field.
    *
    * @param \Drupal\webform\WebformInterface $webform
@@ -175,6 +175,45 @@ public function getMaxSubmissionId(WebformInterface $webform = NULL, EntityInter
    */
   public function hasSubmissionValue(WebformInterface $webform, $element_key);
 
+  /****************************************************************************/
+  // Source entity methods.
+  /****************************************************************************/
+
+  /**
+   * Get total number of source entities.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   *
+   * @return int
+   *   Total number of source entities.
+   */
+  public function getSourceEntitiesTotal(WebformInterface $webform);
+
+  /**
+   * Get source entities associated for a specified webform.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   *
+   * @return array
+   *   An associative array contain source entities associated for
+   *   a specified webform grouped by entity type.
+   */
+  public function getSourceEntities(WebformInterface $webform);
+
+  /**
+   * Get source entities as options for a specified webform.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   *
+   * @return array
+   *   An associative array contain ource entities as options for
+   *   a specified webform.
+   */
+  public function getSourceEntitiesAsOptions(WebformInterface $webform);
+
   /****************************************************************************/
   // Query methods.
   /****************************************************************************/
@@ -299,7 +338,7 @@ public function getSourceEntityTypes(WebformInterface $webform);
    * @param bool $include_elements
    *   Flag that include all form element in the list of columns.
    *
-   * @return array|mixed
+   * @return array
    *   An associative array of columns keyed by name.
    */
   public function getCustomColumns(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE);
@@ -316,13 +355,13 @@ public function getCustomColumns(WebformInterface $webform = NULL, EntityInterfa
    * @param bool $include_elements
    *   Flag that include all form element in the list of columns.
    *
-   * @return array|mixed
+   * @return array
    *   An associative array of columns keyed by name.
    */
   public function getUserColumns(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE);
 
   /**
-   * Get user default submission columns used to display results.
+   * Get default submission columns used to display results.
    *
    * @param \Drupal\webform\WebformInterface|null $webform
    *   A webform.
@@ -333,13 +372,29 @@ public function getUserColumns(WebformInterface $webform = NULL, EntityInterface
    * @param bool $include_elements
    *   Flag that include all form element in the list of columns.
    *
-   * @return array|mixed
-   *   An associative array of columns names.
+   * @return array
+   *   An associative array of columns keyed by name.
    */
-  public function getUserDefaultColumnNames(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE);
+  public function getDefaultColumns(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE);
 
   /**
-   * Get default submission columns used to display results.
+   * Get submissions columns.
+   *
+   * @return array
+   *   An associative array of columns keyed by name.
+   */
+  public function getSubmissionsColumns();
+
+  /**
+   * Get user submissions columns.
+   *
+   * @return array
+   *   An associative array of columns keyed by name.
+   */
+  public function getUsersSubmissionsColumns();
+
+  /**
+   * Get submission columns used to display results table.
    *
    * @param \Drupal\webform\WebformInterface|null $webform
    *   A webform.
@@ -350,13 +405,13 @@ public function getUserDefaultColumnNames(WebformInterface $webform = NULL, Enti
    * @param bool $include_elements
    *   Flag that include all form element in the list of columns.
    *
-   * @return array|mixed
+   * @return array
    *   An associative array of columns keyed by name.
    */
-  public function getDefaultColumns(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE);
+  public function getColumns(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE);
 
   /**
-   * Get submission columns used to display results table.
+   * Get user default submission columns used to display results.
    *
    * @param \Drupal\webform\WebformInterface|null $webform
    *   A webform.
@@ -367,10 +422,31 @@ public function getDefaultColumns(WebformInterface $webform = NULL, EntityInterf
    * @param bool $include_elements
    *   Flag that include all form element in the list of columns.
    *
-   * @return array|mixed
-   *   An associative array of columns keyed by name.
+   * @return array
+   *   An associative array of columns names.
    */
-  public function getColumns(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE);
+  public function getUserDefaultColumnNames(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE);
+
+  /**
+   * Get default submission columns used to display results.
+   *
+   * @param \Drupal\webform\WebformInterface|null $webform
+   *   A webform.
+   * @param \Drupal\Core\Entity\EntityInterface|null $source_entity
+   *   A webform submission source entity.
+   * @param \Drupal\Core\Session\AccountInterface|null $account
+   *   A user account.
+   * @param bool $include_elements
+   *   Flag that include all form element in the list of columns.
+   *
+   * @return array
+   *   An associative array of columns names.
+   */
+  public function getDefaultColumnNames(WebformInterface $webform = NULL, EntityInterface $source_entity = NULL, AccountInterface $account = NULL, $include_elements = TRUE);
+
+  /****************************************************************************/
+  // Custom settings methods.
+  /****************************************************************************/
 
   /**
    * Get customize setting.
@@ -480,10 +556,22 @@ public function saveData(WebformSubmissionInterface $webform_submission, $delete
    *
    * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
    *   A webform submission.
-   * @param array $values
-   *   The value to be logged includes 'handler_id', 'operation', 'message', and 'data'.
-   */
-  public function log(WebformSubmissionInterface $webform_submission, array $values = []);
+   * @param array $context
+   *   The values/context to be logged includes 'handler_id', 'operation', 'message', and 'data'.
+   *
+   * @deprecated Instead call the 'webform_submission' logger channel directly.
+   *
+   *  $message = 'Some message with an %argument.'
+   *  $context = [
+   *    '%argument' => 'Some value'
+   *    'link' => $webform_submission->toLink($this->t('Edit'), 'edit-form')->toString(),
+   *    'webform_submission' => $webform_submission,
+   *    'handler_id' => NULL,
+   *    'data' => [],
+   *  ];
+   *  \Drupal::logger('webform_submission')->notice($message, $context);
+   */
+  public function log(WebformSubmissionInterface $webform_submission, array $context = []);
 
   /****************************************************************************/
   // Draft methods.
diff --git a/web/modules/webform/src/WebformSubmissionStorageSchema.php b/web/modules/webform/src/WebformSubmissionStorageSchema.php
index 0a6e612210fa685353355b88c302455d3e8e0e77..c3705f9aba290da135c7035ac193ab557308e62a 100644
--- a/web/modules/webform/src/WebformSubmissionStorageSchema.php
+++ b/web/modules/webform/src/WebformSubmissionStorageSchema.php
@@ -14,7 +14,7 @@ class WebformSubmissionStorageSchema extends SqlContentEntityStorageSchema {
    * {@inheritdoc}
    */
   protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
-    $schema = parent::getEntitySchema($entity_type, $reset = FALSE);
+    $schema = parent::getEntitySchema($entity_type, $reset);
 
     $schema['webform_submission_data'] = [
       'description' => 'Stores all submitted data for webform submissions.',
@@ -65,75 +65,6 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
       ],
     ];
 
-    $schema['webform_submission_log'] = [
-      'description' => 'Table that contains logs of all webform submission events.',
-      'fields' => [
-        'lid' => [
-          'type' => 'serial',
-          'not null' => TRUE,
-          'description' => 'Primary Key: Unique log event ID.',
-        ],
-        'webform_id' => [
-          'description' => 'The webform id.',
-          'type' => 'varchar',
-          'length' => 32,
-          'not null' => TRUE,
-        ],
-        'sid' => [
-          'description' => 'The webform submission id.',
-          'type' => 'int',
-          'unsigned' => TRUE,
-          'not null' => FALSE,
-        ],
-        'handler_id' => [
-          'description' => 'The webform handler id.',
-          'type' => 'varchar',
-          'length' => 64,
-          'not null' => FALSE,
-        ],
-        'uid' => [
-          'type' => 'int',
-          'unsigned' => TRUE,
-          'not null' => TRUE,
-          'default' => 0,
-          'description' => 'The {users}.uid of the user who triggered the event.',
-        ],
-        'operation' => [
-          'type' => 'varchar_ascii',
-          'length' => 64,
-          'not null' => TRUE,
-          'default' => '',
-          'description' => 'Type of operation, for example "save", "sent", or "update."',
-        ],
-        'message' => [
-          'type' => 'text',
-          'not null' => TRUE,
-          'size' => 'big',
-          'description' => 'Text of log message.',
-        ],
-        'data' => [
-          'type' => 'blob',
-          'not null' => TRUE,
-          'size' => 'big',
-          'description' => 'Serialized array of data.',
-        ],
-        'timestamp' => [
-          'type' => 'int',
-          'not null' => TRUE,
-          'default' => 0,
-          'description' => 'Unix timestamp of when event occurred.',
-        ],
-      ],
-      'primary key' => ['lid'],
-      'indexes' => [
-        'webform_id' => ['webform_id'],
-        'sid' => ['sid'],
-        'uid' => ['uid'],
-        'handler_id' => ['handler_id'],
-        'handler_id_operation' => ['handler_id', 'operation'],
-      ],
-    ];
-
     return $schema;
   }
 
diff --git a/web/modules/webform/src/WebformSubmissionViewBuilder.php b/web/modules/webform/src/WebformSubmissionViewBuilder.php
index 79146369c5a670a3dcac7ff6c9cf194c71d90a8e..6ef69d5c2878e3d2ef92a1b8308c305a9d227a72 100644
--- a/web/modules/webform/src/WebformSubmissionViewBuilder.php
+++ b/web/modules/webform/src/WebformSubmissionViewBuilder.php
@@ -2,14 +2,14 @@
 
 namespace Drupal\webform;
 
-use Drupal\Component\Serialization\Yaml;
 use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityViewBuilder;
 use Drupal\Core\Language\LanguageManagerInterface;
-use Drupal\Core\Render\Element;
 use Drupal\webform\Plugin\WebformElementManagerInterface;
+use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\Utility\WebformYaml;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -23,7 +23,7 @@ class WebformSubmissionViewBuilder extends EntityViewBuilder implements WebformS
    *
    * @var \Drupal\webform\WebformRequestInterface
    */
-  protected $requestManager;
+  protected $requestHandler;
 
   /**
    * The webform element manager service.
@@ -57,7 +57,7 @@ class WebformSubmissionViewBuilder extends EntityViewBuilder implements WebformS
    */
   public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager, WebformRequestInterface $webform_request, WebformElementManagerInterface $element_manager, WebformSubmissionConditionsValidatorInterface $conditions_validator) {
     parent::__construct($entity_type, $entity_manager, $language_manager);
-    $this->requestManager = $webform_request;
+    $this->requestHandler = $webform_request;
     $this->elementManager = $element_manager;
     $this->conditionsValidator = $conditions_validator;
   }
@@ -76,6 +76,20 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getBuildDefaults(EntityInterface $entity, $view_mode) {
+    $build = parent::getBuildDefaults($entity, $view_mode);
+    // The webform submission will be rendered in the wrapped webform submission
+    // template already and thus has no entity template itself.
+    // @see \Drupal\contact_storage\ContactMessageViewBuilder
+    // @see \Drupal\comment\CommentViewBuilder::getBuildDefaults
+    // @see \Drupal\block_content\BlockContentViewBuilder::getBuildDefaults
+    unset($build['#theme']);
+    return $build;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -90,23 +104,29 @@ public function buildComponents(array &$build, array $entities, array $displays,
 
       if ($view_mode === 'preview') {
         $options = [
+          'view_mode' => $view_mode,
           'excluded_elements' => $webform->getSetting('preview_excluded_elements'),
           'exclude_empty' => $webform->getSetting('preview_exclude_empty'),
+          'exclude_empty_checkbox' => $webform->getSetting('preview_exclude_empty_checkbox'),
         ];
       }
       else {
         $options = [
-          'excluded_elements' => $webform->getSetting('excluded_elements'),
-          'exclude_empty' => $webform->getSetting('exclude_empty'),
+          'view_mode' => $view_mode,
+          'excluded_elements' => $webform->getSetting('submission_excluded_elements'),
+          'exclude_empty' => $webform->getSetting('submission_exclude_empty'),
+          'exclude_empty_checkbox' => $webform->getSetting('submission_exclude_empty_checkbox'),
         ];
       }
 
       switch ($view_mode) {
         case 'yaml':
+          // Note that the YAML view ignores all access controls and excluded
+          // settings.
           $data = $webform_submission->toArray(TRUE, TRUE);
           $build[$id]['data'] = [
             '#theme' => 'webform_codemirror',
-            '#code' => WebformYaml::tidy(Yaml::encode($data)),
+            '#code' => WebformYaml::encode($data),
             '#type' => 'yaml',
           ];
           break;
@@ -143,8 +163,7 @@ public function buildElements(array $elements, WebformSubmissionInterface $webfo
     $build = [];
 
     foreach ($elements as $key => $element) {
-      // Make sure this is an element.
-      if (!is_array($element) || Element::property($key)) {
+      if (!WebformElementHelper::isElement($element, $key)) {
         continue;
       }
 
@@ -181,9 +200,16 @@ public function buildTable(array $elements, WebformSubmissionInterface $webform_
       // Replace tokens before building the element.
       $webform_element->replaceTokens($element, $webform_submission);
 
+      // Check if empty value is excluded.
+      if ($webform_element->isEmptyExcluded($element, $options) && !$webform_element->getValue($element, $webform_submission, $options)) {
+        continue;
+      }
+
       $title = $element['#admin_title'] ?: $element['#title'] ?: '(' . $key . ')';
+      // Note: Not displaying an empty message since empty values just render
+      // an empty table cell.
       $html = $webform_element->formatHtml($element, $webform_submission, $options);
-      $rows[] = [
+      $rows[$key] = [
         ['header' => TRUE, 'data' => $title],
         ['data' => (is_string($html)) ? ['#markup' => $html] : $html],
       ];
@@ -193,7 +219,7 @@ public function buildTable(array $elements, WebformSubmissionInterface $webform_
       '#type' => 'table',
       '#rows' => $rows,
       '#attributes' => [
-        'class' => ['webform-submission__table'],
+        'class' => ['webform-submission-table'],
       ],
     ];
   }
@@ -216,7 +242,6 @@ public function buildTable(array $elements, WebformSubmissionInterface $webform_
    *
    * @see \Drupal\webform\WebformSubmissionConditionsValidatorInterface::isElementVisible
    * @see \Drupal\Core\Render\Element::isVisibleElement
-   *
    */
   protected function isElementVisible(array $element, WebformSubmissionInterface $webform_submission, array $options) {
     // Checked excluded elements.
diff --git a/web/modules/webform/src/WebformSubmissionViewsData.php b/web/modules/webform/src/WebformSubmissionViewsData.php
index d64b7abaaf9fc50a7b345e0f11419d50fd52063f..267e3682b6b8deeba4adad6cac946e181e81b0b8 100644
--- a/web/modules/webform/src/WebformSubmissionViewsData.php
+++ b/web/modules/webform/src/WebformSubmissionViewsData.php
@@ -15,6 +15,8 @@ class WebformSubmissionViewsData extends EntityViewsData {
   public function getViewsData() {
     $data = parent::getViewsData();
 
+    $data['webform_submission']['table']['base']['access query tag'] = 'webform_submission_access';
+
     $data['webform_submission']['webform_submission_bulk_form'] = [
       'title' => $this->t('Webform submission operations bulk form'),
       'help' => $this->t('Add a form element that lets you run operations on multiple submissions.'),
diff --git a/web/modules/webform/src/WebformThemeManager.php b/web/modules/webform/src/WebformThemeManager.php
index 050389444c4110670e9858b257b2a63f518d62c2..525c353ce8e6d4788f18fe1832130cc5af1e540d 100644
--- a/web/modules/webform/src/WebformThemeManager.php
+++ b/web/modules/webform/src/WebformThemeManager.php
@@ -4,14 +4,18 @@
 
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Extension\ThemeHandlerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Theme\ThemeManagerInterface;
 use Drupal\Core\Theme\ThemeInitializationInterface;
 
 /**
- * Defines a class to manage webform themeing.
+ * Defines a class to manage webform theming.
  */
 class WebformThemeManager implements WebformThemeManagerInterface {
 
+  use StringTranslationTrait;
+
   /**
    * The configuration object factory.
    *
@@ -26,6 +30,13 @@ class WebformThemeManager implements WebformThemeManagerInterface {
    */
   protected $themeManager;
 
+  /**
+   * The theme handler.
+   *
+   * @var \Drupal\Core\Extension\ThemeHandlerInterface
+   */
+  protected $themeHandler;
+
   /**
    * The theme initialization.
    *
@@ -56,16 +67,45 @@ class WebformThemeManager implements WebformThemeManagerInterface {
    *   The renderer.
    * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
    *   The theme manager.
+   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
+   *   The theme handler.
    * @param \Drupal\Core\Theme\ThemeInitializationInterface $theme_initialization
    *   The theme initialization.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, RendererInterface $renderer, ThemeManagerInterface $theme_manager, ThemeInitializationInterface $theme_initialization) {
+  public function __construct(ConfigFactoryInterface $config_factory, RendererInterface $renderer, ThemeManagerInterface $theme_manager, ThemeHandlerInterface $theme_handler, ThemeInitializationInterface $theme_initialization) {
     $this->configFactory = $config_factory;
     $this->renderer = $renderer;
     $this->themeManager = $theme_manager;
+    $this->themeHandler = $theme_handler;
     $this->themeInitialization = $theme_initialization;
   }
 
+  /**
+   * Get a theme's name.
+   *
+   * @return string
+   *   A theme's name
+   */
+  public function getThemeName($name) {
+    return $this->themeHandler->getName($name);
+  }
+
+  /**
+   * Get themes as associative array.
+   *
+   * @return array
+   *   An associative array containing theme name.
+   */
+  public function getThemeNames() {
+    $themes = ['' => $this->t('Default')];
+    foreach ($this->themeHandler->listInfo() as $name => $theme) {
+      if ($theme->status === 1) {
+        $themes[$name] = $theme->info['name'];
+      }
+    }
+    return $themes;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -85,13 +125,13 @@ public function isActiveTheme($theme_name) {
   /**
    * {@inheritdoc}
    */
-  public function setDefaultTheme() {
+  public function setCurrentTheme($theme_name = NULL) {
     if (!isset($this->activeTheme)) {
       $this->activeTheme = $this->themeManager->getActiveTheme();
     }
-    $default_theme_name = $this->configFactory->get('system.theme')->get('default');
-    $default_theme = $this->themeInitialization->getActiveThemeByName($default_theme_name);
-    $this->themeManager->setActiveTheme($default_theme);
+    $current_theme_name = $this->configFactory->get('system.theme')->get($theme_name ?: 'default');
+    $current_theme = $this->themeInitialization->getActiveThemeByName($current_theme_name);
+    $this->themeManager->setActiveTheme($current_theme);
   }
 
   /**
@@ -106,12 +146,12 @@ public function setActiveTheme() {
   /**
    * {@inheritdoc}
    */
-  public function render(array &$elements, $default_theme = TRUE) {
-    if ($default_theme) {
-      $this->setDefaultTheme();
+  public function render(array &$elements, $theme_name = NULL) {
+    if ($theme_name !== NULL) {
+      $this->setCurrentTheme($theme_name);
     }
     $markup = $this->renderer->render($elements);
-    if ($default_theme) {
+    if ($theme_name !== NULL) {
       $this->setActiveTheme();
     }
     return $markup;
@@ -120,12 +160,12 @@ public function render(array &$elements, $default_theme = TRUE) {
   /**
    * {@inheritdoc}
    */
-  public function renderPlain(array &$elements, $default_theme = TRUE) {
-    if ($default_theme) {
-      $this->setDefaultTheme();
+  public function renderPlain(array &$elements, $theme_name = NULL) {
+    if ($theme_name !== NULL) {
+      $this->setCurrentTheme($theme_name);
     }
     $markup = $this->renderer->renderPlain($elements);
-    if ($default_theme) {
+    if ($theme_name !== NULL) {
       $this->setActiveTheme();
     }
     return $markup;
diff --git a/web/modules/webform/src/WebformThemeManagerInterface.php b/web/modules/webform/src/WebformThemeManagerInterface.php
index 4e8bfb59b2b23e882634562f9f558e603fe6bb2b..179f913818e7679623cc20688080b2ba131a7e9e 100644
--- a/web/modules/webform/src/WebformThemeManagerInterface.php
+++ b/web/modules/webform/src/WebformThemeManagerInterface.php
@@ -7,6 +7,22 @@
  */
 interface WebformThemeManagerInterface {
 
+  /**
+   * Get a theme's name.
+   *
+   * @return string
+   *   A theme's name
+   */
+  public function getThemeName($name);
+
+  /**
+   * Get themes as associative array.
+   *
+   * @return array
+   *   An associative array containing theme name.
+   */
+  public function getThemeNames();
+
   /**
    * Get all active theme names.
    *
@@ -27,9 +43,12 @@ public function getActiveThemeNames();
   public function isActiveTheme($theme_name);
 
   /**
-   * Sets the current theme the default theme.
+   * Sets the current theme the theme.
+   *
+   * @param string $theme_name
+   *   (optional) A theme name. Defaults the default theme.
    */
-  public function setDefaultTheme();
+  public function setCurrentTheme($theme_name = NULL);
 
   /**
    * Sets the current theme the active theme.
diff --git a/web/modules/webform/src/WebformThirdPartySettingsManager.php b/web/modules/webform/src/WebformThirdPartySettingsManager.php
index 57b88f09afb2ce88dc05f0407d9a45e8cb243646..2647ced5ce0e603680bf5be0690054fd769289fa 100644
--- a/web/modules/webform/src/WebformThirdPartySettingsManager.php
+++ b/web/modules/webform/src/WebformThirdPartySettingsManager.php
@@ -67,7 +67,7 @@ public function __construct(ConfigFactoryInterface $config_factory, ModuleHandle
     $this->pathValidator = $path_validator;
     $this->addonsManager = $addons_manager;
 
-    $this->config = $this->configFactory->getEditable('webform.settings');
+    $this->config = $this->configFactory->get('webform.settings');
     $this->loadIncludes();
   }
 
diff --git a/web/modules/webform/src/WebformTokenManager.php b/web/modules/webform/src/WebformTokenManager.php
index 7a8821c72a3ef398912c1c45f4ce4fd045c634f0..74efbf7bd5b98d9f201eb8e68c365f02958c8b47 100644
--- a/web/modules/webform/src/WebformTokenManager.php
+++ b/web/modules/webform/src/WebformTokenManager.php
@@ -2,10 +2,16 @@
 
 namespace Drupal\webform;
 
+use Drupal\Component\Utility\Xss;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Theme\ThemeManagerInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
 use Drupal\Core\Utility\Token;
 use Drupal\webform\Utility\WebformFormHelper;
 
@@ -14,6 +20,22 @@
  */
 class WebformTokenManager implements WebformTokenManagerInterface {
 
+  use StringTranslationTrait;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
   /**
    * The configuration object factory.
    *
@@ -29,35 +51,51 @@ class WebformTokenManager implements WebformTokenManagerInterface {
   protected $moduleHandler;
 
   /**
-   * The theme manager.
+   * The token service.
    *
-   * @var \Drupal\Core\Theme\ThemeManagerInterface
+   * @var \Drupal\Core\Utility\Token
    */
-  protected $themeManager;
+  protected $token;
 
   /**
-   * The token service.
+   * An array of support token suffixes.
    *
-   * @var \Drupal\Core\Utility\Token
+   * @var array
+   *
+   * @see webform_token_info_alter()
    */
-  protected $token;
+  static protected $suffixes = [
+    // Removes the token when not replaced.
+    'clear',
+    // Decodes HTML entities.
+    'htmldecode',
+    // Removes all HTML tags from the token's value.
+    'striptags',
+    // URL encodes the token's value.
+    'urlencode',
+    // XML encodes the token's value.
+    'xmlencode',
+  ];
 
   /**
    * Constructs a WebformTokenManager object.
    *
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The configuration object factory.
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler.
-   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
-   *   The theme manager.
    * @param \Drupal\Core\Utility\Token $token
    *   The token service.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, Token $token) {
+  public function __construct(AccountInterface $current_user, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, Token $token) {
+    $this->currentUser = $current_user;
+    $this->languageManager = $language_manager;
     $this->configFactory = $config_factory;
     $this->moduleHandler = $module_handler;
-    $this->themeManager = $theme_manager;
     $this->token = $token;
 
     $this->config = $this->configFactory->get('webform.settings');
@@ -66,11 +104,11 @@ public function __construct(ConfigFactoryInterface $config_factory, ModuleHandle
   /**
    * {@inheritdoc}
    */
-  public function replace($text, EntityInterface $entity = NULL, array $data = [], array $options = []) {
+  public function replace($text, EntityInterface $entity = NULL, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata = NULL) {
     // Replace tokens within an array.
     if (is_array($text)) {
-      foreach ($text as $key => $value) {
-        $text[$key] = $this->replace($value, $entity, $data, $options);
+      foreach ($text as $key => $token_value) {
+        $text[$key] = $this->replace($token_value, $entity, $data, $options);
       }
       return $text;
     }
@@ -86,44 +124,135 @@ public function replace($text, EntityInterface $entity = NULL, array $data = [],
 
       // Set token data based on entity type.
       $this->setTokenData($data, $entity);
+
+      // Set token options based on entity.
+      $this->setTokenOptions($options, $entity);
     }
 
-    // Track the active theme, if there is no active theme it means tokens
-    // are being replaced during the initial page request.
-    $has_active_theme = $this->themeManager->hasActiveTheme();
+    // For anonymous users remove all [current-user] tokens to prevent
+    // anonymous user properties from being displayed.
+    // For example, the [current-user:display-name] token will return
+    // 'Anonymous', which is not an expected behavior.
+    if ($this->currentUser->isAnonymous() && strpos($text, '[current-user:') !== FALSE) {
+      $text = preg_replace('/\[current-user:[^]]+\]/', '', $text);
+    }
+
+    // Get supported suffixes.
+    $suffixes = $this->getSuffixes($options);
+
+    // Prepare suffixes.
+    $text = $this->prepareSuffixes($text, $suffixes);
+
+    // Replace the webform related tokens.
+    $text = $this->token->replace($text, $data, $options, $bubbleable_metadata);
 
-    // Replace the tokens.
-    $result = $this->token->replace($text, $data, $options);
+    // Process token suffixes.
+    $text = $this->processSuffixes($text);
 
-    // If there was no active theme and now there is one.
-    // Reset the active theme, so that theme negotiators can determine the
-    // correct active theme.
-    if (!$has_active_theme && $this->themeManager->hasActiveTheme()) {
-      $this->themeManager->resetActiveTheme();
+    // Clear current user tokens for undefined values.
+    if (strpos($text, '[current-user:') !== FALSE) {
+      $text = preg_replace('/\[current-user:[^\]]+\]/', '', $text);
     }
 
-    return $result;
+    return $text;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function buildTreeLink(array $token_types = ['webform', 'webform_submission', 'webform_handler'], $description = NULL) {
+  public function replaceNoRenderContext($text, EntityInterface $entity = NULL, array $data = [], array $options = []) {
+    // Create BubbleableMetadata object which will be ignored.
+    $bubbleable_metadata = new BubbleableMetadata();
+    return $this->replace($text, $entity, $data, $options, $bubbleable_metadata);
+  }
+
+  /**
+   * Get token data based on an entity's type.
+   *
+   * @param array $data
+   *   An array of token data.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   A Webform, Webform submission entity, or other entity.
+   */
+  protected function setTokenData(array &$data, EntityInterface $entity) {
+    if ($entity instanceof WebformSubmissionInterface) {
+      $data['webform_submission'] = $entity;
+      $data['webform'] = $entity->getWebform();
+    }
+    elseif ($entity instanceof WebformInterface) {
+      $data['webform'] = $entity;
+    }
+    else {
+      $data[$entity->getEntityTypeId()] = $entity;
+    }
+  }
+
+  /**
+   * Set token option based on the entity.
+   *
+   * @param array $options
+   *   An array of token data.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   A Webform or Webform submission entity.
+   */
+  protected function setTokenOptions(array &$options, EntityInterface $entity) {
+    $token_options = [];
+    if ($entity instanceof WebformSubmissionInterface) {
+      $token_options['langcode'] = $entity->language()->getId();
+    }
+    elseif ($entity instanceof WebformInterface) {
+      $token_options['langcode'] = $this->languageManager->getCurrentLanguage()->getId();
+    }
+    $options += $token_options;
+  }
+
+  /****************************************************************************/
+  // Token elements.
+  /****************************************************************************/
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildTreeLink(array $token_types = ['webform', 'webform_submission', 'webform_handler']) {
+    if (!$this->moduleHandler->moduleExists('token')) {
+      return [
+        '#type' => 'link',
+        '#title' => $this->t('You may use tokens.'),
+        '#url' => Url::fromUri('https://www.drupal.org/project/token'),
+      ];
+    }
+    else {
+      return [
+        '#theme' => 'token_tree_link',
+        '#text' => $this->t('You may use tokens.'),
+        '#token_types' => $token_types,
+        '#click_insert' => TRUE,
+        '#dialog' => TRUE,
+        '#attached' => ['library' => ['webform/webform.token']],
+      ];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildTreeElement(array $token_types = ['webform', 'webform_submission', 'webform_handler'], $description = NULL) {
     if (!$this->moduleHandler->moduleExists('token')) {
       return [];
     }
 
-    // @todo Issue #2235581: Make Token Dialog support inserting in WYSIWYGs.
     $build = [
       '#theme' => 'token_tree_link',
       '#token_types' => $token_types,
-      '#click_insert' => FALSE,
+      '#click_insert' => TRUE,
       '#dialog' => TRUE,
+      '#attached' => ['library' => ['webform/webform.token']],
     ];
 
     if ($description) {
       if ($this->config->get('ui.description_help')) {
         return [
+          '#type' => 'container',
           'token_tree_link' => $build,
           'help' => [
             '#type' => 'webform_help',
@@ -133,6 +262,7 @@ public function buildTreeLink(array $token_types = ['webform', 'webform_submissi
       }
       else {
         return [
+          '#type' => 'container',
           'token_tree_link' => $build,
           'description' => [
             '#prefix' => ' ',
@@ -142,10 +272,17 @@ public function buildTreeLink(array $token_types = ['webform', 'webform_submissi
       }
     }
     else {
-      return $build;
+      return [
+        '#type' => 'container',
+        'token_tree_link' => $build,
+      ];
     }
   }
 
+  /****************************************************************************/
+  // Token validation.
+  /****************************************************************************/
+
   /**
    * {@inheritdoc}
    */
@@ -170,33 +307,150 @@ public function elementValidate(array &$form, array $token_types = ['webform', '
       'webform_select_other' => 'webform_select_other',
       'webform_radios_other' => 'webform_radios_other',
     ];
+
+    // If $form render array is an element then see if we should add
+    // validation callback.
+    if (isset($form['#type']) && isset($text_element_types[$form['#type']])) {
+      $form['#element_validate'][] = [get_called_class(), 'validateElement'];
+      $form['#token_types'] = $token_types;
+    }
+
     $elements =& WebformFormHelper::flattenElements($form);
     foreach ($elements as &$element) {
       if (!isset($element['#type']) || !isset($text_element_types[$element['#type']])) {
         continue;
       }
 
-      $element['#element_validate'][] = 'token_element_validate';
+      $element['#element_validate'][] = [get_called_class(), 'validateElement'];
       $element['#token_types'] = $token_types;
     }
   }
 
   /**
-   * Get token data based on an entity's type.
+   * Validates an element's tokens.
    *
-   * @param array $token_data
-   *   An array of token data.
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   A Webform or Webform submission entity.
+   * Note:
+   * Element is not being based by reference since the #value is being altered.
    */
-  protected function setTokenData(array &$token_data, EntityInterface $entity) {
-    if ($entity instanceof WebformSubmissionInterface) {
-      $token_data['webform_submission'] = $entity;
-      $token_data['webform'] = $entity->getWebform();
+  public static function validateElement($element, FormStateInterface $form_state, &$complete_form) {
+    $value = isset($element['#value']) ? $element['#value'] : $element['#default_value'];
+
+    if (!mb_strlen($value)) {
+      return $element;
     }
-    elseif ($entity instanceof WebformInterface) {
-      $token_data['webform'] = $entity;
+
+    // Remove suffixes which are not valid.
+    $element['#value'] = preg_replace('/\[(webform[^]]+)((?::' . implode('|:', static::$suffixes) . ')+)\]/', '[\1]', $value);
+
+    // Convert all token field deltas to 0 to prevent unexpected
+    // token validation errors.
+    $element['#value'] = preg_replace('/:\d+:/', ':0:', $element['#value']);
+
+    token_element_validate($element, $form_state);
+  }
+
+  /****************************************************************************/
+  // Suffix handling.
+  /****************************************************************************/
+
+  /**
+   * Get an array of supported token suffixes.
+   *
+   * @param array $options
+   *   A keyed array of settings and flags to control the token
+   *   replacement process.
+   *
+   * @return array
+   *   An array of supported token suffixes,
+   */
+  protected function getSuffixes(array $options) {
+    $suffixes = static::$suffixes;
+    // Unset any $option['suffixes'] set to FALSE.
+    if (isset($options['suffixes'])) {
+      foreach ($suffixes as $index => $suffix) {
+        if (isset($options['suffixes'][$suffix]) && $options['suffixes'][$suffix] === FALSE) {
+          unset($suffixes[$index]);
+        }
+      }
+    }
+    return $suffixes;
+  }
+
+  /**
+   * Prepare token suffixes to be replaced and processed.
+   *
+   * Prepare token suffixes by wrapping them in temp
+   * {webform-token-suffixes} tags.
+   *
+   * [webform:token:clear:urlencode] becomes
+   * {webform-token-suffixes:clear:urlencode}[webform:token]{/webform-token-suffixes}.
+   *
+   * @param string|array $text
+   *   A string of text that may contain tokens.
+   * @param array $suffixes
+   *   An array of supported suffixes.
+   *
+   * @return string
+   *   A string of text with token suffixes wrapped in
+   *   {webform-token-suffixes} tags.
+   */
+  protected function prepareSuffixes($text, array $suffixes) {
+    if (preg_match_all('/\[(.+?)((?::' . implode('|:', $suffixes) . ')+)\]/', $text, $matches)) {
+      foreach ($matches[0] as $index => $match) {
+        $value = $matches[1][$index];
+        $suffixes = $matches[2][$index];
+        $wrapper = '{webform-token-suffixes' . $suffixes . '}[' . $value . ']{/webform-token-suffixes}';
+        $text = str_replace($match, $wrapper, $text);
+      }
+    }
+    return $text;
+  }
+
+  /**
+   * Process token suffixes after all tokens are replaced.
+   *
+   * @param string|array $text
+   *   A string of text that may contain {webform-token-suffixes} tags.
+   *
+   * @return string
+   *   String to text with all tokens suffixes processed.
+   */
+  protected function processSuffixes($text) {
+    if (preg_match_all('/{webform-token-suffixes:([^}]+)}(.*?){\/webform-token-suffixes}/ms', $text, $matches)) {
+      foreach ($matches[0] as $index => $match) {
+        $search = $matches[0][$index];
+        $replace = $matches[2][$index];
+
+        $value = $matches[2][$index];
+        $suffixes = explode(':', $matches[1][$index]);
+        $suffixes = array_combine($suffixes, $suffixes);
+
+        // If token is not replaced then only the :clear suffix is applicable.
+        if (preg_match('/^\[[^}]+\]$/', $value)) {
+          // Clear token text or restore the original token.
+          $original = str_replace(']', ':' . $matches[1][$index] . ']', $value);
+          $replace = (isset($suffixes['clear'])) ? '' : $original;
+        }
+        else {
+          // Decode and XSS filter value first.
+          if (isset($suffixes['htmldecode'])) {
+            $replace = html_entity_decode($replace);
+            $replace = (isset($suffixes['striptags'])) ? strip_tags($replace) : html_entity_decode(Xss::filterAdmin($replace));
+          }
+          // Encode URL.
+          if (isset($suffixes['urlencode'])) {
+            $replace = urlencode($replace);
+          }
+          // Encode xml.
+          if (isset($suffixes['xmlencode'])) {
+            $replace = htmlspecialchars($replace, ENT_XML1);
+          }
+        }
+
+        $text = str_replace($search, $replace, $text);
+      }
     }
+    return $text;
   }
 
 }
diff --git a/web/modules/webform/src/WebformTokenManagerInterface.php b/web/modules/webform/src/WebformTokenManagerInterface.php
index 9ebe444122f2e1c9846576664ec9946160570f73..4eef6ed235c0d213148bf05826b2d9f3ab596a04 100644
--- a/web/modules/webform/src/WebformTokenManagerInterface.php
+++ b/web/modules/webform/src/WebformTokenManagerInterface.php
@@ -3,6 +3,7 @@
 namespace Drupal\webform;
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Render\BubbleableMetadata;
 
 /**
  * Defines an interface for token manager classes.
@@ -27,23 +28,69 @@ interface WebformTokenManagerInterface {
    *     array of token replacements after they are generated.
    *   - clear: A boolean flag indicating that tokens should be removed from the
    *     final text if no replacement value can be generated.
+   * @param \Drupal\Core\Render\BubbleableMetadata|null $bubbleable_metadata
+   *   (optional) An object to which static::generate() and the hooks and
+   *   functions that it invokes will add their required bubbleable metadata.
    *
    * @return string|array
    *   Text or array with tokens replaced.
    *
    * @see \Drupal\Core\Utility\Token::replace
    */
-  public function replace($text, EntityInterface $entity = NULL, array $data = [], array $options = []);
+  public function replace($text, EntityInterface $entity = NULL, array $data = [], array $options = [], BubbleableMetadata $bubbleable_metadata = NULL);
+
+  /**
+   * Replace tokens in text with no render context.
+   *
+   * This method allows tokens to be replaced when there is no render context
+   * via REST and JSON API requests.
+   *
+   * @param string|array $text
+   *   A string of text that may contain tokens.
+   * @param \Drupal\Core\Entity\EntityInterface|null $entity
+   *   A Webform or Webform submission entity.
+   * @param array $data
+   *   (optional) An array of keyed objects.
+   * @param array $options
+   *   (optional) A keyed array of settings and flags to control the token
+   *   replacement process. Supported options are:
+   *   - langcode: A language code to be used when generating locale-sensitive
+   *     tokens.
+   *   - callback: A callback function that will be used to post-process the
+   *     array of token replacements after they are generated.
+   *   - clear: A boolean flag indicating that tokens should be removed from the
+   *     final text if no replacement value can be generated.
+   *
+   * @return string|array
+   *   Text or array with tokens replaced.
+   *
+   * @see \Drupal\Core\Utility\Token::replace
+   */
+  public function replaceNoRenderContext($text, EntityInterface $entity = NULL, array $data = [], array $options = []);
 
   /**
    * Build token tree link if token.module is installed.
    *
    * @param array $token_types
    *   An array containing token types that should be shown in the tree.
+   *
+   * @return array
+   *   A render array containing a token tree link.
+   */
+  public function buildTreeLink(array $token_types = ['webform', 'webform_submission']);
+
+  /**
+   * Build token tree element if token.module is installed.
+   *
+   * @param array $token_types
+   *   An array containing token types that should be shown in the tree.
    * @param string $description
    *   (optional) Description to appear after the token tree link.
+   *
+   * @return array
+   *   A render array containing a token tree link wrapped in a div.
    */
-  public function buildTreeLink(array $token_types = ['webform', 'webform_submission'], $description = NULL);
+  public function buildTreeElement(array $token_types = ['webform', 'webform_submission'], $description = NULL);
 
   /**
    * Validate form that should have tokens in it.
diff --git a/web/modules/webform/src/WebformTranslationManager.php b/web/modules/webform/src/WebformTranslationManager.php
index cc8436eb6dae6ad46a9e8cba02202561b2276dbb..684c19f89219ead6ffb60e999c0c5d361254e8de 100644
--- a/web/modules/webform/src/WebformTranslationManager.php
+++ b/web/modules/webform/src/WebformTranslationManager.php
@@ -2,19 +2,29 @@
 
 namespace Drupal\webform;
 
+use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Serialization\Yaml;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\webform\Plugin\WebformElement\WebformCompositeBase;
 use Drupal\webform\Plugin\WebformElementManagerInterface;
 use Drupal\webform\Utility\WebformArrayHelper;
 use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\Utility\WebformYaml;
+use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
  * Defines a class to translate webform elements.
  */
 class WebformTranslationManager implements WebformTranslationManagerInterface {
 
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
   /**
    * The language manager.
    *
@@ -43,22 +53,54 @@ class WebformTranslationManager implements WebformTranslationManagerInterface {
    */
   protected $translatableProperties;
 
+  /**
+   * The messenger.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
   /**
    * Constructs a WebformTranslationManager object.
    *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
    *   The language manager.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The configuration object factory.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger.
    * @param \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager
    *   The webform element manager.
    */
-  public function __construct(LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, WebformElementManagerInterface $element_manager) {
+  public function __construct(RouteMatchInterface $route_match, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, MessengerInterface $messenger, WebformElementManagerInterface $element_manager) {
+    $this->routeMatch = $route_match;
     $this->languageManager = $language_manager;
     $this->configFactory = $config_factory;
+    $this->messenger = $messenger;
     $this->elementManager = $element_manager;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isAdminRoute() {
+    $route_name = $this->routeMatch->getRouteName();
+
+    // Don't initialize translation on webform CRUD routes.
+    if (preg_match('/^entity\.webform\.(?:edit_form|duplicate_form|delete_form)$/', $route_name)) {
+      return TRUE;
+    }
+
+    // Don't initialize translation on webform UI routes.
+    if (strpos($route_name, 'entity.webform_ui.') === 0) {
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -86,7 +128,7 @@ public function getElements(WebformInterface $webform, $langcode = NULL, $reset
       return [];
     }
     elseif ($error = WebformYaml::validate($elements)) {
-      drupal_set_message($error, 'error');
+      $this->messenger->addError($error);
       return [];
     }
     else {
@@ -102,6 +144,18 @@ public function getBaseElements(WebformInterface $webform) {
     $config_elements = $this->getElements($webform, $default_langcode);
     $elements = WebformElementHelper::getFlattened($config_elements);
     foreach ($elements as $element_key => &$element) {
+      // Always include composite element's default '#{element}__title'.
+      $element_plugin = $this->elementManager->getElementInstance($element);
+      if ($element_plugin instanceof WebformCompositeBase) {
+        $composite_elements = $element_plugin->getCompositeElements();
+        foreach ($composite_elements as $composite_key => $composite_element) {
+          $property_key = $composite_key . '__title';
+          if (empty($element["#$property_key"])) {
+            $element["#$property_key"] = $element_plugin->getDefaultProperty($property_key);
+          }
+        }
+      }
+
       $this->removeUnTranslatablePropertiesFromElement($element);
       if (empty($element)) {
         unset($elements[$element_key]);
@@ -153,7 +207,7 @@ public function getOriginalLangcode(WebformInterface $webform) {
    *   An element.
    */
   protected function removeUnTranslatablePropertiesFromElement(array &$element) {
-    $translatable_properties = $this->getTranslatableProperies();
+    $translatable_properties = $this->getTranslatableProperties();
 
     $element_type = (isset($element['#type'])) ? $element['#type'] : NULL;
     foreach ($element as $property_key => $property_value) {
@@ -187,7 +241,7 @@ protected function removeUnTranslatablePropertiesFromElement(array &$element) {
    * @return array
    *   An array of translated properties prefixed with a hashes (#).
    */
-  protected function getTranslatableProperies() {
+  protected function getTranslatableProperties() {
     if ($this->translatableProperties) {
       return $this->translatableProperties;
     }
diff --git a/web/modules/webform/src/WebformTranslationManagerInterface.php b/web/modules/webform/src/WebformTranslationManagerInterface.php
index ba81f31c61b1778fa4bc26cf751173814cda331b..2c724e5b3c4ccfa205684eaab03e37b60978f8c3 100644
--- a/web/modules/webform/src/WebformTranslationManagerInterface.php
+++ b/web/modules/webform/src/WebformTranslationManagerInterface.php
@@ -7,6 +7,14 @@
  */
 interface WebformTranslationManagerInterface {
 
+  /**
+   * Determine if the translated webform should be displayed.
+   *
+   * @return bool
+   *   TRUE if the translated webform should be displayed.
+   */
+  public function isAdminRoute();
+
   /**
    * Get webform elements for specific language.
    *
diff --git a/web/modules/webform/templates/webform-composite-address.html.twig b/web/modules/webform/templates/webform-composite-address.html.twig
index c71432eb9441b699a55b843e132160018d448d39..e6ddb7db4b338af556af734d0ff912b8569d15e2 100644
--- a/web/modules/webform/templates/webform-composite-address.html.twig
+++ b/web/modules/webform/templates/webform-composite-address.html.twig
@@ -13,7 +13,6 @@
  * @ingroup themeable
  */
 #}
-{{ attach_library('webform/webform.composite') }}
 {% if flexbox %}
 
   {% if content.address %}
diff --git a/web/modules/webform/templates/webform-composite-contact.html.twig b/web/modules/webform/templates/webform-composite-contact.html.twig
index 52f124fc1036dca0bc0cfbb5f090fa649560b351..15bd443fae61f8ff51184fdfa93eac7367911a8c 100644
--- a/web/modules/webform/templates/webform-composite-contact.html.twig
+++ b/web/modules/webform/templates/webform-composite-contact.html.twig
@@ -13,7 +13,6 @@
  * @ingroup themeable
  */
 #}
-{{ attach_library('webform/webform.composite') }}
 {% if flexbox %}
 
   {% if content.name or content.company %}
diff --git a/web/modules/webform/templates/webform-composite-link.html.twig b/web/modules/webform/templates/webform-composite-link.html.twig
index b30ef739dff508fe6189ba62f81c56ae5be728d9..30fdb40907817852d673dcd3a92fe104b9b3b58a 100644
--- a/web/modules/webform/templates/webform-composite-link.html.twig
+++ b/web/modules/webform/templates/webform-composite-link.html.twig
@@ -11,5 +11,4 @@
  * @ingroup themeable
  */
 #}
-{{ attach_library('webform/webform.composite') }}
 {{ content }}
diff --git a/web/modules/webform/templates/webform-composite-location.html.twig b/web/modules/webform/templates/webform-composite-location.html.twig
index 18cb15649dd5fadca516cf5d91cd282718601da0..84eb2cd17964599a9ab4b01c69da14a340af1d0b 100644
--- a/web/modules/webform/templates/webform-composite-location.html.twig
+++ b/web/modules/webform/templates/webform-composite-location.html.twig
@@ -11,5 +11,4 @@
  * @ingroup themeable
  */
 #}
-{{ attach_library('webform/webform.composite') }}
 {{ content }}
diff --git a/web/modules/webform/templates/webform-composite-name.html.twig b/web/modules/webform/templates/webform-composite-name.html.twig
index d84a553578351fe7b8e8eb7a5e5bcfda7e6cbd3b..6a55d243a94cc7e9eb009b6b85f40db647b882dd 100644
--- a/web/modules/webform/templates/webform-composite-name.html.twig
+++ b/web/modules/webform/templates/webform-composite-name.html.twig
@@ -13,7 +13,6 @@
  * @ingroup themeable
  */
 #}
-{{ attach_library('webform/webform.composite') }}
 {% if flexbox %}
   <div class="webform-flexbox">
     {% if content.title %}
diff --git a/web/modules/webform/templates/webform-composite-telephone.html.twig b/web/modules/webform/templates/webform-composite-telephone.html.twig
index a5c766739e520c21eef78197937e5d2e67fcfaaa..939fcf5d7f49980024fb8ccdb0b8f714ab92ab35 100644
--- a/web/modules/webform/templates/webform-composite-telephone.html.twig
+++ b/web/modules/webform/templates/webform-composite-telephone.html.twig
@@ -13,8 +13,6 @@
  * @ingroup themeable
  */
 #}
-{{ attach_library('webform/webform.composite.telephone') }}
-
 {{ content.type }}
 {{ content.phone }}
 {{ content.ext }}
diff --git a/web/modules/webform/templates/webform-confirmation.html.twig b/web/modules/webform/templates/webform-confirmation.html.twig
index 0547fe05ad99110abef167962e585b3e8075e977..c5db7adfb615686c1786e2bb54bf3117335300c0 100644
--- a/web/modules/webform/templates/webform-confirmation.html.twig
+++ b/web/modules/webform/templates/webform-confirmation.html.twig
@@ -27,7 +27,7 @@
 
   {% if back %}
   <div class="webform-confirmation__back">
-    <a href="{{ back_url }}" rel="back" title="{{ back_label }}"{{ back_attributes }}>{{ back_label }}</a>
+    <a href="{{ back_url }}" rel="prev" title="{{ back_label }}"{{ back_attributes }}>{{ back_label }}</a>
   </div>
   {% endif %}
 
diff --git a/web/modules/webform/templates/webform-element-help.html.twig b/web/modules/webform/templates/webform-element-help.html.twig
index 5dac43c06c24a9a7b36e0318ec56886a03d6d693..167433f8950639262102bbda231d2cdb79d0ad58 100644
--- a/web/modules/webform/templates/webform-element-help.html.twig
+++ b/web/modules/webform/templates/webform-element-help.html.twig
@@ -15,6 +15,6 @@
 #}
 {% spaceless %}
 {{ attach_library('webform/webform.element.help') }}
-{{ help_icon }}
+<span{{ attributes }}><span aria-hidden="true">?</span></span>
 {% endspaceless %}
 
diff --git a/web/modules/webform/templates/webform-element-more.html.twig b/web/modules/webform/templates/webform-element-more.html.twig
index f9f12cb819bc74835f4425a7924a7f51e0abb985..458449469d15c908da390b9ef9395b0a902ab943 100644
--- a/web/modules/webform/templates/webform-element-more.html.twig
+++ b/web/modules/webform/templates/webform-element-more.html.twig
@@ -7,8 +7,11 @@
  * - title: More label.
  * - content: More content.
  *
- * @see template_preprocess_webform_element_more()
+ * Based on WAI-ARIA Authoring Practices 1.1: Disclosure (Show/Hide)
  *
+ * @see https://www.w3.org/TR/wai-aria-practices-1.1/#disclosure
+ * @see https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-faq.html
+ * @see template_preprocess_webform_element_more()
  * @ingroup themeable
  */
 #}
@@ -20,6 +23,6 @@ set classes = [
 ]
 %}
 <div{{ attributes.addClass(classes) }}>
-  <div class="webform-element-more--link"><a href="#more">{{ more_title }}</a></div>
-  <div class="webform-element-more--content">{{ more }}</div>
+  <div class="webform-element-more--link"><a role="button" href="#more">{{ more_title }}</a></div>
+  <div id="{{ attributes.id }}--content" class="webform-element-more--content">{{ more }}</div>
 </div>
diff --git a/web/modules/webform/templates/webform-email-html.html.twig b/web/modules/webform/templates/webform-email-html.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..c13483b72d004ad6b776f72d1f7189fb30f8fea3
--- /dev/null
+++ b/web/modules/webform/templates/webform-email-html.html.twig
@@ -0,0 +1,16 @@
+{#
+/**
+ * @file
+ * Default theme implementation for webform email wrapper template as HTML.
+ *
+ * @ingroup themeable
+ */
+#}
+<html>
+<head>
+  <title>{{ subject }}</title>
+</head>
+<body>
+{{ body|raw }}
+</body>
+</html>
diff --git a/web/modules/webform/templates/webform-email-message-html.html.twig b/web/modules/webform/templates/webform-email-message-html.html.twig
index c3954117c419a4df7a007c55420148df00a3efdd..ee6a5966c9bafef710eb56f06d66bc8aab84d849 100644
--- a/web/modules/webform/templates/webform-email-message-html.html.twig
+++ b/web/modules/webform/templates/webform-email-message-html.html.twig
@@ -4,7 +4,7 @@
  * Default theme implementation for webform email wrapper template as HTML.
  *
  * Available variables:
- * - message: The email message which contains to, from, subject, message, etc...
+ * - message: The email message which contains to, from, subject, message, etc…
  * - webform_submission: The webform submission.
  * - handler: The webform handler.
  *
diff --git a/web/modules/webform/templates/webform-email-message-text.html.twig b/web/modules/webform/templates/webform-email-message-text.html.twig
index dad0ddfcedd5eab7dca6817b9663c6c330d93867..7474c4bd22c2c0df4b9a600ed0e48e432d1b26e3 100644
--- a/web/modules/webform/templates/webform-email-message-text.html.twig
+++ b/web/modules/webform/templates/webform-email-message-text.html.twig
@@ -4,7 +4,7 @@
  * Default theme implementation for webform email wrapper template as plain text.
  *
  * Available variables:
- * - message: The email message which contains to, from, subject, message, etc...
+ * - message: The email message which contains to, from, subject, message, etc…
  * - webform_submission: The webform submission.
  * - handler: The webform handler.
  *
diff --git a/web/modules/webform/templates/webform-handler-email-summary.html.twig b/web/modules/webform/templates/webform-handler-email-summary.html.twig
index 69cd89ad4085b4288916e5186efde82661a11cf3..9984f4c7c2e910924af8cc191a7d056f32771125 100644
--- a/web/modules/webform/templates/webform-handler-email-summary.html.twig
+++ b/web/modules/webform/templates/webform-handler-email-summary.html.twig
@@ -16,5 +16,6 @@
 {% if settings.bcc_mail %}<b>{{ 'BCC:'|t }}</b> {{ settings.bcc_mail }}<br />{% endif %}
 <b>{{ 'From:'|t }}</b> {% if settings.from_name %}{{ settings.from_name }}{% endif %} &lt;{{ settings.from_mail }}&gt;<br />
 <b>{{ 'Subject:'|t }}</b> {{ settings.subject }}<br />
-<b>{{ 'Settings:'|t }}</b> {{ settings.html ? 'HTML' : 'Plain text'|t }} {{ settings.html and settings.attachments ? '/' : '' }}{{ settings.attachments ? 'Attachments'|t : '' }} {{ settings.twig ? '(Twig)'|t : '' }}<br />
+<b>{{ 'Settings:'|t }}</b> {{ settings.html ? 'HTML' : 'Plain text'|t }} {{ settings.html and settings.attachments ? '/' : '' }} {{ settings.attachments ? 'Attachments'|t : '' }} {{ settings.twig ? '(Twig)'|t : '' }}<br />
 <b>{{ 'Sent when:'|t }}</b> {% if settings.states %}{{ settings.states|join('; ') }}{% else %}{{ 'Custom'|t }}{% endif %}<br />
+{% if settings.theme_name %}<b>{{ 'Theme:'|t }}</b> {{ settings.theme_name }}<br />{% endif %}
diff --git a/web/modules/webform/templates/webform-help-video-youtube.html.twig b/web/modules/webform/templates/webform-help-video-youtube.html.twig
index 45198f834a7e38d9abe32b5f81c923d7a7be1e45..b27cf7f71da9d28671433ae27a68c1ab3bd92c0c 100644
--- a/web/modules/webform/templates/webform-help-video-youtube.html.twig
+++ b/web/modules/webform/templates/webform-help-video-youtube.html.twig
@@ -12,6 +12,6 @@
 {{ attach_library('webform/webform.help') }}
 <div class="webform-help-video-youtube">
   <div class="webform-help-video-youtube--container">
-    <iframe width="560" height="315" src="https://www.youtube.com/embed/{{ youtube_id }}{{ autoplay ? '?autoplay=1' : '' }}" frameborder="0" allowfullscreen></iframe>
+    <iframe width="560" height="315" src="https://www.youtube.com/embed/{{ youtube_id }}?rel=0&modestbranding=1{{ autoplay ? '&autoplay=1' : '' }}"{{ autoplay ? 'allow="autoplay"' : '' }} frameborder="0" allowfullscreen></iframe>
   </div>
 </div>
diff --git a/web/modules/webform/templates/webform-html-editor-markup.html.twig b/web/modules/webform/templates/webform-html-editor-markup.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..2560548dc248d1b547b8829ce6f9347f21eb51fa
--- /dev/null
+++ b/web/modules/webform/templates/webform-html-editor-markup.html.twig
@@ -0,0 +1,21 @@
+{#
+/**
+ * @file
+ * Theme implementation for webform html editor markup.
+ *
+ * Available variables
+ * - markup: HTML markup.
+ * - allowed_tags: Allowed tags.
+ * - content: Renderable HTML markup.
+ *
+ * Using Twig output modifer to remove extra carriage returns at the end of
+ * the generated markup.
+ *
+ * @see template_preprocess_webform_html_editor_markup()
+ * @see \Drupal\webform\Element\WebformHtmlEditor::checkMarkup
+ * @see https://www.drupal.org/project/drupal/issues/2245901
+ * @see https://twig.symfony.com/doc/2.x/templates.html#whitespace-control
+ * @ingroup themeable
+ */
+#}
+{{- content -}}
diff --git a/web/modules/webform/templates/webform-progress-tracker.html.twig b/web/modules/webform/templates/webform-progress-tracker.html.twig
index da1f3cba3903594da6b4516f2ac2e966d51c5096..2de6d271584a7adcbf2db3f17085281af4bec048 100644
--- a/web/modules/webform/templates/webform-progress-tracker.html.twig
+++ b/web/modules/webform/templates/webform-progress-tracker.html.twig
@@ -12,6 +12,7 @@
  * - max_pages: Maximum number of pages that progress text should be displayed on.
  *
  * @see template_preprocess_webform_progress_bar()
+ * @see https://www.w3.org/WAI/tutorials/forms/multi-page/
  *
  * @ingroup themeable
  */
@@ -20,20 +21,27 @@
 
 <ul class="webform-progress-tracker progress-tracker progress-tracker--center">
   {% for index, page in progress %}
-  {%
-    set classes = [
-      'progress-step',
-      index < current_index ? 'is-complete',
-      index == current_index ? 'is-active',
-    ]
-  %}
-  <li{{ attributes.setAttribute('data-webform-page', page.name).setAttribute('class', '').addClass(classes) }}>
-    <span class="progress-marker">{{ index + 1 }}</span>
-    {% if progress|length < max_pages %}
-      <span class="progress-text">
-        <div class="progress-title">{{ page.title }}</div>
-      </span>
-    {% endif %}
-  </li>
+    {% set is_completed = index < current_index %}
+    {% set is_active = index == current_index %}
+    {%
+      set classes = [
+        'progress-step',
+        is_completed ? 'is-complete',
+        is_active ? 'is-active',
+      ]
+    %}
+    <li{{ attributes.setAttribute('data-webform-page', page.name).setAttribute('title', page.title).setAttribute('class', '').addClass(classes) }}>
+      <span class="progress-marker">{{ index + 1 }}</span>
+      {% if progress|length < max_pages %}
+        <span class="progress-text">
+          <span class="progress-title">
+            {% if is_active or is_completed %}
+              <span class="visually-hidden">{{ is_active ? 'Current'|t : 'Completed'|t }}: </span>
+            {% endif %}
+            {{ page.title }}
+          </span>
+        </span>
+      {% endif %}
+    </li>
   {% endfor %}
 </ul>
diff --git a/web/modules/webform/templates/webform-submission-form.html.twig b/web/modules/webform/templates/webform-submission-form.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..0969e22da7458edba10d4830127e529ef437a642
--- /dev/null
+++ b/web/modules/webform/templates/webform-submission-form.html.twig
@@ -0,0 +1,12 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a webform submission form.
+ *
+ * Available variables:
+ * - form: The webform submission form.
+ *
+ * @ingroup themeable
+ */
+#}
+{{ form }}
diff --git a/web/modules/webform/templates/webform-submission-information.html.twig b/web/modules/webform/templates/webform-submission-information.html.twig
index 2fdd38125003154583a3297f8f4a1ddc12f49bc5..a85e5bf806d17d527186117b08c2fc3a7b2567d6 100644
--- a/web/modules/webform/templates/webform-submission-information.html.twig
+++ b/web/modules/webform/templates/webform-submission-information.html.twig
@@ -4,62 +4,80 @@
  * Default theme implementation for webform submission information.
  *
  * Available variables:
- * @todo Add variables.
+ * - serial: The serial number of the submission entity.
+ * - sid: The ID of the submission entity.
+ * - uuid: The UUID of the submission entity.
+ * - token: A secure token used to look up a submission.
+ * - uri: The URI the user submitted the webform.
+ * - created: The time that the submission was first saved.
+ * - completed: The time that the submission was completed.
+ * - changed: The time that the submission was saved.
+ * - in_draft: Is this a draft of the submission?
+ * - current_page: The current wizard page.
+ * - remote_addr: The IP address of the user that submitted the webform.
+ * - submitted_by: The user that submitted the webform.
+ * - submitted_to: Link to the submission's URI.
+ * - langcode: The submission language code.
+ * - locked: A flag that indicates a locked submission.
+ * - sticky: A flag that indicate the status of the submission.
+ * - notes: Administrative notes about the submission.
+ * - webform: The associated webform.
+ * - token_update: The tokenize URL used to update the submission.
+ * - delete: Link to delete the submission.
  *
  * @see template_preprocess_webform_submission_information()
  *
  * @ingroup themeable
  */
 #}
-<details data-webform-element-id="{{ webform_id }}--submission_information"{% if open %} open="open"{% endif %}>
-  <summary role="button"{% if open %} aria-expanded="true" aria-pressed="true"{% endif %}>{{ 'Submission information'|t }}</summary>
-  <div class="details-wrapper">
-    {% if submissions_view %}
-      <div><b>{{ 'Submission Number'|t }}:</b> {{ serial }}</div>
-      <div><b>{{ 'Submission ID'|t }}:</b> {{ sid }}</div>
-      <div><b>{{ 'Submission UUID'|t }}:</b> {{ uuid }}</div>
-      {% if uri %}
-        <div><b>{{ 'Submission URI'|t }}:</b> {{ uri }}</div>
-      {% endif %}
-      {% if token_update %}
-        <div><b>{{ 'Submission Update'|t }}:</b> {{ token_update }}</div>
-      {% endif %}
-      <br />
-      <div><b>{{ 'Created'|t }}:</b> {{ created }}</div>
-      <div><b>{{ 'Completed'|t }}:</b> {{ completed }}</div>
-      <div><b>{{ 'Changed'|t }}:</b> {{ changed }}</div>
-      <br />
-      <div><b>{{ 'Remote IP address'|t }}:</b> {{ remote_addr }}</div>
-      <div><b>{{ 'Submitted by'|t }}:</b> {{ submitted_by }}</div>
-      <div><b>{{ 'Language'|t }}:</b> {{ language }}</div>
-      <br />
-      <div><b>{{ 'Is draft'|t }}:</b> {{ is_draft }}</div>
-      {% if current_page %}
-        <div><b>{{ 'Current page'|t }}:</b> {{ current_page }}</div>
-      {% endif %}
-      <div><b>{{ 'Webform'|t }}:</b> {{ webform }}</div>
-      {% if submitted_to %}
-        <div><b>{{ 'Submitted to'|t }}:</b> {{ submitted_to }}</div>
-      {% endif %}
-      {% if sticky or locked or notes %}
-        <br />
-        {% if sticky %}
-          <div><b>{{ 'Flagged'|t }}:</b> {{ sticky }}</div>
-        {% endif %}
-        {% if locked %}
-          <div><b>{{ 'Locked'|t }}:</b> {{ locked }}</div>
-        {% endif %}
-        {% if notes %}
-          <div><b>{{ 'Notes'|t }}:</b><br/>
-          <pre>{{ notes }}</pre>
-          </div>
-        {% endif %}
-      {% endif %}
-
-    {% else %}
-      <div><b>{{ 'Submission Number'|t }}:</b> {{ serial }}</div>
-      <div><b>{{ 'Created'|t }}:</b> {{ created }}</div>
+{% if submissions_view %}
+  <div><b>{{ 'Submission Number'|t }}:</b> {{ serial }}</div>
+  <div><b>{{ 'Submission ID'|t }}:</b> {{ sid }}</div>
+  <div><b>{{ 'Submission UUID'|t }}:</b> {{ uuid }}</div>
+  {% if uri %}
+    <div><b>{{ 'Submission URI'|t }}:</b> {{ uri }}</div>
+  {% endif %}
+  {% if token_update %}
+    <div><b>{{ 'Submission Update'|t }}:</b> {{ token_update }}</div>
+  {% endif %}
+  <br />
+  <div><b>{{ 'Created'|t }}:</b> {{ created }}</div>
+  <div><b>{{ 'Completed'|t }}:</b> {{ completed }}</div>
+  <div><b>{{ 'Changed'|t }}:</b> {{ changed }}</div>
+  <br />
+  <div><b>{{ 'Remote IP address'|t }}:</b> {{ remote_addr }}</div>
+  <div><b>{{ 'Submitted by'|t }}:</b> {{ submitted_by }}</div>
+  <div><b>{{ 'Language'|t }}:</b> {{ language }}</div>
+  <br />
+  <div><b>{{ 'Is draft'|t }}:</b> {{ is_draft }}</div>
+  {% if current_page %}
+    <div><b>{{ 'Current page'|t }}:</b> {{ current_page }}</div>
+  {% endif %}
+  <div><b>{{ 'Webform'|t }}:</b> {{ webform }}</div>
+  {% if submitted_to %}
+    <div><b>{{ 'Submitted to'|t }}:</b> {{ submitted_to }}</div>
+  {% endif %}
+  {% if sticky or locked or notes %}
+    <br />
+    {% if sticky %}
+      <div><b>{{ 'Flagged'|t }}:</b> {{ sticky }}</div>
+    {% endif %}
+    {% if locked %}
+      <div><b>{{ 'Locked'|t }}:</b> {{ locked }}</div>
     {% endif %}
+    {% if notes %}
+      <div><b>{{ 'Notes'|t }}:</b><br/>
+      <pre>{{ notes }}</pre>
+      </div>
+    {% endif %}
+  {% endif %}
+
+{% else %}
+  <div><b>{{ 'Submission Number'|t }}:</b> {{ serial }}</div>
+  <div><b>{{ 'Created'|t }}:</b> {{ created }}</div>
+{% endif %}
 
-  </div>
-</details>
+{% if delete %}
+  <br/>
+  <div>{{ delete }}</div>
+{% endif %}
diff --git a/web/modules/webform/templates/webform-submission-navigation.html.twig b/web/modules/webform/templates/webform-submission-navigation.html.twig
index 5e6562424334549d274fa940ba68eb38dfd17088..131c3b0f49d6dec36191b19e0d9c32f2247bf6c0 100644
--- a/web/modules/webform/templates/webform-submission-navigation.html.twig
+++ b/web/modules/webform/templates/webform-submission-navigation.html.twig
@@ -7,6 +7,7 @@
  * - prev_url: URL to the previous webform submission.
  * - next_url: URL to the next webform submission.
  * - webform_id: The webform ID. Provided for context.
+ * - webform_title: The webform title. Provided for context.
  *
  * @see template_preprocess_webform_submission_navigation()
  *
@@ -15,6 +16,7 @@
 #}
 {% if prev_url or next_url %}
   <nav id="webform-submission-navigation-{{ webform_id }}" class="webform-submission-navigation" role="navigation" aria-labelledby="webform-submission-label-{{ webform_id }}">
+    <h2 class="visually-hidden" id="webform-submission-label-{{ webform_id }}">Submission navigation links for {{ webform_title }}</h2>
     <ul class="webform-submission-pager">
       {% if prev_url %}
         <li class="webform-submission-pager__item webform-submission-pager__item--previous">
diff --git a/web/modules/webform/templates/webform-submission.html.twig b/web/modules/webform/templates/webform-submission.html.twig
index 65cb9dfb08a4fa41c0860735e5173eac4a77de2d..1675c0b876beb2a39b5cda8670939da47824e1cb 100644
--- a/web/modules/webform/templates/webform-submission.html.twig
+++ b/web/modules/webform/templates/webform-submission.html.twig
@@ -22,5 +22,7 @@ view_mode ? 'webform-submission--view-mode-' ~ view_mode|clean_class,
 ]
 %}
 <div{{ attributes.addClass(classes) }}>
-{{ elements }}
+{{ navigation }}
+{{ information }}
+{{ submission }}
 </div>
diff --git a/web/modules/webform/tests/files/sample.html b/web/modules/webform/tests/files/sample.html
index bbda772c556cfba1e92b3001413128a55cc2c2be..59950347d380dc47d1a3f386d2a76d195dc6fbca 100644
--- a/web/modules/webform/tests/files/sample.html
+++ b/web/modules/webform/tests/files/sample.html
@@ -5,7 +5,7 @@
 </head>
 
 <body>
-The content of the document......
+The content of the document……
 </body>
 
-</html>
\ No newline at end of file
+</html>
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax.yml
index 190033eeb95f52562e3fb4aabc21c1f916fde75e..cbb11887f55cd312517f5c758805e2b141f0d236 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_ajax
 title: 'Test: Ajax'
 description: 'Test Ajax enabled form with preview'
@@ -25,6 +27,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -32,6 +35,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -47,22 +51,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -73,6 +92,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -91,9 +111,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -146,4 +168,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_inline.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_inline.yml
index 133ef3809271eb05823ada8550cf224ad6186a64..641ef9624387629983fa915e04fa9e14c0e2b817 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_inline.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_inline.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_ajax_confirmation_inline
 title: 'Test: Ajax: Confirmation: Inline'
 description: 'Test Ajax inline confirmation message.'
@@ -26,6 +28,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +36,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +52,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +93,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -92,9 +112,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -147,4 +169,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_message.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_message.yml
index 195a416e260d4c9240fb87db12a0f4a4ed7889a7..f3c68649da1f61d9a3d1a6fe61d275fc9718b785 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_message.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_message.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_ajax_confirmation_message
 title: 'Test: Ajax: Confirmation: Message'
 description: 'Test Ajax basic confirmation message.'
@@ -26,6 +28,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +36,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +52,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +93,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -92,9 +112,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -147,4 +169,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_modal.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_modal.yml
index dd164c27a4f3e5800c76bee3add82ce471dab7e3..8a0d97671a3bf510e89ea3848ccebb065c19402f 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_modal.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_modal.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_ajax_confirmation_modal
 title: 'Test: Ajax: Confirmation: Modal'
 description: 'Test Ajax confirmation modal.'
@@ -26,6 +28,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +36,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +52,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +93,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -92,9 +112,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -147,4 +169,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_page.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_page.yml
index cdd03d3a6fe7f65da130dd8a2ceb5cf25deb0f81..63c2a406d12b101fea23edd0c2c670595870e3d1 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_page.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_page.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_ajax_confirmation_page
 title: 'Test: Ajax: Confirmation: Page'
 description: 'Test Ajax redirecting to a confirmation page.'
@@ -26,6 +28,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +36,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +52,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +93,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -92,9 +112,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -147,4 +169,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_url.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_url.yml
index e792b5fc325f2da0537e039c0457f3c29327e170..196feec04371be2f8d4af484ad933e9625f6015d 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_url.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_url.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_ajax_confirmation_url
 title: 'Test: Ajax: Confirmation: URL'
 description: 'Test redirecting to an internal URL.'
@@ -23,6 +25,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -30,6 +33,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -45,22 +49,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -71,6 +90,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -89,9 +109,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -144,4 +166,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_url_msg.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_url_msg.yml
index 485e6f605288e172ee1a23d5a5aedf2456560ec3..2bd95a8ddae398ef535d693e69587c773c504b37 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_url_msg.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_ajax_confirmation_url_msg.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_ajax_confirmation_url_msg
 title: 'Test: Ajax: Confirmation: URL with message'
 description: 'Test Ajax redirecting to an internal URL with a confirmation message.'
@@ -26,6 +28,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +36,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +52,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +93,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -92,9 +112,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -147,4 +169,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite.yml
index e3fd983eb7ef2ffcebeca8165c4493b1ce73e436..3c46471ec04ff9e55eecfaf4926f9a1e86d2a5a2 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_composite
 title: 'Test: Composite: Composites'
 description: 'Test composite elements, includes address, contact, and name.'
@@ -71,10 +73,10 @@ elements: |
       '#country__access': false
     link_basic:
       '#type': webform_link
-      '#title': 'Link'
+      '#title': Link
       '#default_value':
         title: Example
-        url: http://example.com
+        url: 'http://example.com'
   composite_elements_multiple:
     '#type': details
     '#title': 'Composite Elements Multiple'
@@ -86,7 +88,7 @@ elements: |
       '#multiple__header': true
       '#default_value':
         - title: Example
-          url: http://example.com
+          url: 'http://example.com'
       '#title__help': 'This is link title help'
       '#url__help': 'This is link url help'
 css: ''
@@ -97,6 +99,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -104,6 +107,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -119,22 +123,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -145,6 +164,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -163,9 +183,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -218,6 +240,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_custom.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_custom.yml
index 119bf7db020c4f408666542f97b18f91b61364fe..a9108968074e4034d6930fc3b105fa49f7196465 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_custom.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_custom.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_composite_custom
 title: 'Test: Composite: Custom composite'
 description: 'Test custom composite element.'
@@ -16,8 +18,6 @@ elements: |
   webform_custom_composite_basic:
     '#type': webform_custom_composite
     '#title': webform_custom_composite_basic
-    '#multiple': false
-    '#multiple_header': false
     '#element':
       first_name:
         '#type': textfield
@@ -26,8 +26,8 @@ elements: |
         '#type': textfield
         '#title': 'Last name'
     '#default_value':
-       - first_name: John
-         last_name: Smith
+      - first_name: John
+        last_name: Smith
   webform_custom_composite_advanced:
     '#type': webform_custom_composite
     '#title': webform_custom_composite_advanced
@@ -54,17 +54,17 @@ elements: |
         '#title': 'Employment status'
       age:
         '#type': number
-        '#title': 'Age'
+        '#title': Age
         '#field_suffix': ' yrs. old'
         '#min': 1
         '#max': 125
     '#default_value':
-       - first_name: John
-         last_name: Smith
-         gender: Male
-         martial_status: Single
-         employment_status: Unemployed
-         age: 20
+      - first_name: John
+        last_name: Smith
+        gender: Male
+        martial_status: Single
+        employment_status: Unemployed
+        age: 20
 css: ''
 javascript: ''
 settings:
@@ -73,6 +73,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -80,6 +81,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -95,22 +97,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -121,6 +138,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -139,9 +157,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -194,6 +214,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_custom_file.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_custom_file.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d48daa88deb0cff7508a1f36e34cac1b9d54d5eb
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_custom_file.yml
@@ -0,0 +1,200 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_composite_custom_file
+title: 'Test: Composite: Custom composite file'
+description: 'Test custom composite file element.'
+category: 'Test: Composite'
+elements: |
+  webform_custom_composite_file:
+    '#type': webform_custom_composite
+    '#title': webform_custom_composite_file
+    '#multiple__header': false
+    '#element':
+      textfield:
+        '#type': textfield
+        '#title': textfield
+      managed_file:
+        '#type': managed_file
+        '#title': managed_file
+  webform_custom_composite_file_advanced:
+    '#type': webform_custom_composite
+    '#title': webform_custom_composite_file_advanced
+    '#element':
+      textfield:
+        '#type': textfield
+        '#title': textfield
+      managed_file:
+        '#type': managed_file
+        '#title': managed_file
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 0
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_format.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_format.yml
index 35c76a8a04de1799a36aa1bda0faac08169ccf65..96a3234ae7fbb255a9c9530fb99e311efbe76ed8 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_format.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_format.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_composite_format
 title: 'Test: Composite: Format composite'
 description: 'Test composite element formatting.'
@@ -115,40 +117,85 @@ elements: |
     '#type': details
     '#title': 'Composite elements'
     '#open': true
+    address:
+      '#type': details
+      '#title': 'Advanced address'
+      address_value:
+        '#type': address
+        '#title': 'Advanced address (Value)'
+        '#default_value':
+          given_name: John
+          family_name: Smith
+          organization: 'Google Inc.'
+          address_line1: '1098 Alta Ave'
+          postal_code: '94043'
+          locality: 'Mountain View'
+          administrative_area: CA
+          country_code: US
+          langcode: en
+        '#format': value
+      address_raw:
+        '#type': address
+        '#title': 'Advanced address (Raw value)'
+        '#default_value':
+          given_name: John
+          family_name: Smith
+          organization: 'Google Inc.'
+          address_line1: '1098 Alta Ave'
+          postal_code: '94043'
+          locality: 'Mountain View'
+          administrative_area: CA
+          country_code: US
+          langcode: en
+        '#format': raw
+      address_list:
+        '#type': address
+        '#title': 'Advanced address (List)'
+        '#default_value':
+          given_name: John
+          family_name: Smith
+          organization: 'Google Inc.'
+          address_line1: '1098 Alta Ave'
+          postal_code: '94043'
+          locality: 'Mountain View'
+          administrative_area: CA
+          country_code: US
+          langcode: en
+        '#format': list
     webform_address:
       '#type': details
-      '#title': Address
+      '#title': 'Basic address'
       webform_address_value:
         '#type': webform_address
-        '#title': 'Address (Value)'
+        '#title': 'Basic address (Value)'
         '#default_value':
           address: '10 Main Street'
           address_2: '10 Main Street'
           city: Springfield
           state_province: Alabama
-          postal_code: Loremipsum
+          postal_code: '11111'
           country: Afghanistan
         '#format': value
       webform_address_raw:
         '#type': webform_address
-        '#title': 'Address (Raw value)'
+        '#title': 'Basic address (Raw value)'
         '#default_value':
           address: '10 Main Street'
           address_2: '10 Main Street'
           city: Springfield
           state_province: Alabama
-          postal_code: Loremipsum
+          postal_code: '11111'
           country: Afghanistan
         '#format': raw
       webform_address_list:
         '#type': webform_address
-        '#title': 'Address (List)'
+        '#title': 'Basic address (List)'
         '#default_value':
           address: '10 Main Street'
           address_2: '10 Main Street'
           city: Springfield
           state_province: Alabama
-          postal_code: Loremipsum
+          postal_code: '11111'
           country: Afghanistan
         '#format': list
     webform_contact:
@@ -166,7 +213,7 @@ elements: |
           address_2: '10 Main Street'
           city: Springfield
           state_province: Alabama
-          postal_code: Loremipsum
+          postal_code: '11111'
           country: Afghanistan
         '#format': value
       webform_contact_raw:
@@ -181,7 +228,7 @@ elements: |
           address_2: '10 Main Street'
           city: Springfield
           state_province: Alabama
-          postal_code: Loremipsum
+          postal_code: '11111'
           country: Afghanistan
         '#format': raw
       webform_contact_list:
@@ -196,7 +243,7 @@ elements: |
           address_2: '10 Main Street'
           city: Springfield
           state_province: Alabama
-          postal_code: Loremipsum
+          postal_code: '11111'
           country: Afghanistan
         '#format': list
     webform_link:
@@ -223,35 +270,35 @@ elements: |
           title: Loremipsum
           url: 'http://example.com'
         '#format': list
-    webform_location:
+    webform_location_geocomplete:
       '#type': details
       '#title': Location
-      webform_location_value:
-        '#type': webform_location
+      webform_location_geocomplete_value:
+        '#type': webform_location_places
         '#title': 'Location (Value)'
         '#map': true
         '#geolocation': true
         '#format': value
         '#default_value':
           value: 'The White House, 1600 Pennsylvania Ave NW, Washington, DC 20500, USA'
-      webform_location_raw:
-        '#type': webform_location
+      webform_location_geocomplete_raw:
+        '#type': webform_location_places
         '#title': 'Location (Raw value)'
         '#map': true
         '#geolocation': true
         '#format': raw
         '#default_value':
           value: 'The White House, 1600 Pennsylvania Ave NW, Washington, DC 20500, USA'
-      webform_location_list:
-        '#type': webform_location
+      webform_location_geocomplete_list:
+        '#type': webform_location_places
         '#title': 'Location (List)'
         '#map': true
         '#geolocation': true
         '#format': list
         '#default_value':
           value: 'The White House, 1600 Pennsylvania Ave NW, Washington, DC 20500, USA'
-      webform_location_map:
-        '#type': webform_location
+      webform_location_geocomplete_map:
+        '#type': webform_location_places
         '#title': 'Location (Map)'
         '#map': true
         '#geolocation': true
@@ -400,6 +447,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -407,6 +455,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -422,22 +471,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -448,6 +512,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -466,9 +531,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -521,6 +588,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_text:
     id: email
@@ -532,23 +603,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: false
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -564,23 +637,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_format_multiple.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_format_multiple.yml
index 9c9c70a3bdf2ca14dbb7a66961ca2b7674ea1356..0b2ec9d80b6ea0d1518c255c2c695e17dcf90b1a 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_format_multiple.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_composite_format_multiple.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_composite_format_multiple
 title: 'Test: Composite: Format composite multiple values'
 description: 'Test composite element formatting with multiple value.'
@@ -17,103 +19,166 @@ elements: |
     '#type': details
     '#title': 'Composite elements'
     '#open': true
+    address:
+      '#type': details
+      '#title': 'Advanced address'
+      address_ol:
+        '#type': address
+        '#title': 'Advanced address (Ordered list)'
+        '#multiple': true
+        '#default_value':
+          - given_name: John
+            family_name: Smith
+            organization: 'Google Inc.'
+            address_line1: '1098 Alta Ave'
+            postal_code: '94043'
+            locality: 'Mountain View'
+            administrative_area: CA
+            country_code: US
+            langcode: en
+        '#format_items': ol
+      address_ul:
+        '#type': address
+        '#title': 'Advanced address (Unordered list)'
+        '#multiple': true
+        '#default_value':
+          - given_name: John
+            family_name: Smith
+            organization: 'Google Inc.'
+            address_line1: '1098 Alta Ave'
+            postal_code: '94043'
+            locality: 'Mountain View'
+            administrative_area: CA
+            country_code: US
+            langcode: en
+        '#format_items': ul
+      address_hr:
+        '#type': address
+        '#title': 'Advanced address (Horizontal rule)'
+        '#multiple': true
+        '#default_value':
+          - given_name: John
+            family_name: Smith
+            organization: 'Google Inc.'
+            address_line1: '1098 Alta Ave'
+            postal_code: '94043'
+            locality: 'Mountain View'
+            administrative_area: CA
+            country_code: US
+            langcode: en
+        '#format_items': hr
+      address_table:
+        '#type': address
+        '#title': 'Advanced address (Table)'
+        '#multiple': true
+        '#default_value':
+          - given_name: John
+            family_name: Smith
+            organization: 'Google Inc.'
+            address_line1: '1098 Alta Ave'
+            postal_code: '94043'
+            locality: 'Mountain View'
+            administrative_area: CA
+            country_code: US
+            langcode: en
+        '#format_items': table
     webform_address:
       '#type': details
-      '#title': Address
+      '#title': 'Basic address'
       webform_address_ol:
         '#type': webform_address
-        '#title': 'Address (Ordered list)'
+        '#title': 'Basic address (Ordered list)'
         '#multiple': true
         '#default_value':
           - address: '10 Main Street'
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - address: '10 Main Street'
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - address: '10 Main Street'
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
         '#format_items': ol
       webform_address_ul:
         '#type': webform_address
-        '#title': 'Address (Unordered list)'
+        '#title': 'Basic address (Unordered list)'
         '#multiple': true
         '#default_value':
           - address: '10 Main Street'
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - address: '10 Main Street'
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - address: '10 Main Street'
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
         '#format_items': ul
       webform_address_hr:
         '#type': webform_address
-        '#title': 'Address (Horizontal rule)'
+        '#title': 'Basic address (Horizontal rule)'
         '#multiple': true
         '#default_value':
           - address: '10 Main Street'
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - address: '10 Main Street'
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - address: '10 Main Street'
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
         '#format_items': hr
       webform_address_table:
         '#type': webform_address
-        '#title': 'Address (Table)'
+        '#title': 'Basic address (Table)'
         '#multiple': true
         '#default_value':
           - address: '10 Main Street'
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - address: '10 Main Street'
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - address: '10 Main Street'
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
         '#format_items': table
     webform_contact:
@@ -132,7 +197,7 @@ elements: |
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - name: Loremipsum
             company: Loremipsum
@@ -142,7 +207,7 @@ elements: |
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - name: Loremipsum
             company: Loremipsum
@@ -152,7 +217,7 @@ elements: |
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
         '#format_items': ol
       webform_contact_ul:
@@ -168,7 +233,7 @@ elements: |
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - name: Loremipsum
             company: Loremipsum
@@ -178,7 +243,7 @@ elements: |
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - name: Loremipsum
             company: Loremipsum
@@ -188,7 +253,7 @@ elements: |
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
         '#format_items': ul
       webform_contact_hr:
@@ -204,7 +269,7 @@ elements: |
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - name: Loremipsum
             company: Loremipsum
@@ -214,7 +279,7 @@ elements: |
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - name: Loremipsum
             company: Loremipsum
@@ -224,7 +289,7 @@ elements: |
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
         '#format_items': hr
       webform_contact_table:
@@ -240,7 +305,7 @@ elements: |
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - name: Loremipsum
             company: Loremipsum
@@ -250,7 +315,7 @@ elements: |
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
           - name: Loremipsum
             company: Loremipsum
@@ -260,9 +325,108 @@ elements: |
             address_2: '10 Main Street'
             city: Springfield
             state_province: Alabama
-            postal_code: Loremipsum
+            postal_code: '11111'
             country: Afghanistan
         '#format_items': table
+    webform_custom_composite:
+      '#type': details
+      '#title': 'Custom composite'
+      webform_custom_composite_ol:
+        '#type': webform_custom_composite
+        '#title': 'Custom composite (Ordered list)'
+        '#element':
+          name:
+            '#type': textfield
+            '#title': Name
+            '#title_display': invisible
+          gender:
+            '#type': select
+            '#title': Gender
+            '#title_display': invisible
+            '#options':
+              Male: Male
+              Female: Female
+        '#multiple': true
+        '#default_value':
+          - name: Loremipsum
+            gender: Male
+          - name: Loremipsum
+            gender: Male
+          - name: Loremipsum
+            gender: Male
+        '#format_items': ol
+      webform_custom_composite_ul:
+        '#type': webform_custom_composite
+        '#title': 'Custom composite (Unordered list)'
+        '#element':
+          name:
+            '#type': textfield
+            '#title': Name
+            '#title_display': invisible
+          gender:
+            '#type': select
+            '#title': Gender
+            '#title_display': invisible
+            '#options':
+              Male: Male
+              Female: Female
+        '#multiple': true
+        '#default_value':
+          - name: Loremipsum
+            gender: Male
+          - name: Loremipsum
+            gender: Male
+          - name: Loremipsum
+            gender: Male
+        '#format_items': ul
+      webform_custom_composite_hr:
+        '#type': webform_custom_composite
+        '#title': 'Custom composite (Horizontal rule)'
+        '#element':
+          name:
+            '#type': textfield
+            '#title': Name
+            '#title_display': invisible
+          gender:
+            '#type': select
+            '#title': Gender
+            '#title_display': invisible
+            '#options':
+              Male: Male
+              Female: Female
+        '#multiple': true
+        '#default_value':
+          - name: Loremipsum
+            gender: Male
+          - name: Loremipsum
+            gender: Male
+          - name: Loremipsum
+            gender: Male
+        '#format_items': hr
+      webform_custom_composite_table:
+        '#type': webform_custom_composite
+        '#title': 'Custom composite (Table)'
+        '#element':
+          name:
+            '#type': textfield
+            '#title': Name
+            '#title_display': invisible
+          gender:
+            '#type': select
+            '#title': Gender
+            '#title_display': invisible
+            '#options':
+              Male: Male
+              Female: Female
+        '#multiple': true
+        '#default_value':
+          - name: Loremipsum
+            gender: Male
+          - name: Loremipsum
+            gender: Male
+          - name: Loremipsum
+            gender: Male
+        '#format_items': table
     webform_link:
       '#type': details
       '#title': Link
@@ -314,11 +478,11 @@ elements: |
           - title: Loremipsum
             url: 'http://example.com'
         '#format_items': table
-    webform_location:
+    webform_location_geocomplete:
       '#type': details
       '#title': Location
-      webform_location_ol:
-        '#type': webform_location
+      webform_location_geocomplete_ol:
+        '#type': webform_location_places
         '#title': 'Location (Ordered list)'
         '#map': true
         '#geolocation': true
@@ -329,8 +493,8 @@ elements: |
           - value: 'London SW1A 1AA, United Kingdom'
           - value: 'Moscow, Russia, 10307'
         '#format_items': ol
-      webform_location_ul:
-        '#type': webform_location
+      webform_location_geocomplete_ul:
+        '#type': webform_location_places
         '#title': 'Location (Unordered list)'
         '#map': true
         '#geolocation': true
@@ -341,8 +505,8 @@ elements: |
           - value: 'London SW1A 1AA, United Kingdom'
           - value: 'Moscow, Russia, 10307'
         '#format_items': ul
-      webform_location_hr:
-        '#type': webform_location
+      webform_location_geocomplete_hr:
+        '#type': webform_location_places
         '#title': 'Location (Horizontal rule)'
         '#map': true
         '#geolocation': true
@@ -353,8 +517,8 @@ elements: |
           - value: 'London SW1A 1AA, United Kingdom'
           - value: 'Moscow, Russia, 10307'
         '#format_items': hr
-      webform_location_table:
-        '#type': webform_location
+      webform_location_geocomplete_table:
+        '#type': webform_location_places
         '#title': 'Location (Table)'
         '#map': true
         '#geolocation': true
@@ -535,6 +699,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -542,6 +707,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -557,22 +723,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -583,6 +764,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -601,9 +783,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -656,6 +840,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_text:
     id: email
@@ -667,23 +855,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: false
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -699,23 +889,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_inline.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_inline.yml
index f3caadd1f1796d120a4f5ccea8efdf0742f84e6d..1a4043191e57db6cc377bd927a3a34e42ee2ae2e 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_inline.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_inline.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_confirmation_inline
 title: 'Test: Confirmation: Inline'
 description: 'Test inline confirmation message.'
@@ -26,6 +28,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +36,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +52,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +93,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -92,9 +112,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -147,4 +169,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_message.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_message.yml
index 1fe89577d1715c79ac0cf527f564ed8a4d0bfcb6..dbc73a001c74305702091713f7dfc5eacedd2d8b 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_message.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_message.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_confirmation_message
 title: 'Test: Confirmation: Message'
 description: 'Test basic confirmation message.'
@@ -26,6 +28,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +36,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +52,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +93,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -92,9 +112,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -147,4 +169,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_modal.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_modal.yml
index cf074c708e511eea68394bdee7303adafca167d4..2e7481f158652b6de8607830a014715308302d4e 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_modal.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_modal.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_confirmation_modal
 title: 'Test: Confirmation: Modal'
 description: 'Test confirmation modal.'
@@ -26,6 +28,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +36,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +52,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +93,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -92,9 +112,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -147,4 +169,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_none.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_none.yml
new file mode 100644
index 0000000000000000000000000000000000000000..94dc1bc366c501a34dc878329361c40738f89a4d
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_none.yml
@@ -0,0 +1,176 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_confirmation_none
+title: 'Test: Confirmation: None'
+description: 'Test no confirmation message.'
+category: 'Test: Confirmation'
+elements: |
+  description:
+    '#markup': 'This webform will not display a confirmation when submitted.'
+  test:
+    '#type': textfield
+    '#title': test
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: none
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_page.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_page.yml
index ec776fd4dee79c2b52cfd5a8c54253c454747600..58ab287f4713a826c8b07b5671af113cb8ec4803 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_page.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_page.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_confirmation_page
 title: 'Test: Confirmation: Page'
 description: 'Test redirecting to a confirmation page.'
@@ -26,6 +28,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +36,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +52,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +93,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -92,9 +112,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -147,4 +169,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_page_custom.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_page_custom.yml
index d021656f6b5b1955b49f3cbb34589c7831a43121..c4bd7d0f9ffc8a8b486bcb2bc10d358863ea9aa9 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_page_custom.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_page_custom.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_confirmation_page_custom
 title: 'Test: Confirmation: Page custom'
 description: 'Test redirecting to a customized confirmation page.'
@@ -26,6 +28,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +36,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +52,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +93,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -95,9 +115,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -150,4 +172,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_url.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_url.yml
index 3f42f81c06d6ea18f736443aa84132f60b309454..84aaad2485d8b85f60aa16c8f3b54c2e5bb66007 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_url.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_url.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_confirmation_url
 title: 'Test: Confirmation: URL'
 description: 'Test redirecting to an internal URL.'
@@ -23,6 +25,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -30,6 +33,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -45,22 +49,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -71,6 +90,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -89,9 +109,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -144,4 +166,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_url_message.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_url_message.yml
index eb7f71df85a317b5e4a5f071a26b8572d375b4fd..5e069d93600784984922367f171f05c643258065 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_url_message.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_confirmation_url_message.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_confirmation_url_message
 title: 'Test: Confirmation: URL with message'
 description: 'Test redirecting to an internal URL with a confirmation message.'
@@ -26,6 +28,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +36,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +52,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +93,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -92,9 +112,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -147,4 +169,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element.yml
index fc64914048935596564c1825add68ec292c266e9..0d72dbd281eba016d8bb07caac9ac13910be712d 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element
 title: 'Test: Element'
 description: 'Test instances of common webform elements.'
@@ -21,6 +23,9 @@ elements: |
     '#type': value
     '#title': value
     '#value': '{value}'
+  empty:
+    '#type': textfield
+    '#title': empty
   markup_elements:
     '#type': details
     '#title': 'Markup Elements'
@@ -58,6 +63,7 @@ elements: |
       '#default_value': |
         {textarea line 1}
         {textarea line 2}
+        
     textfield:
       '#type': textfield
       '#title': textfield
@@ -239,6 +245,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -246,6 +253,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -261,22 +269,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -287,6 +310,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -305,9 +329,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -360,4 +386,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_access.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_access.yml
index 179b65581aad336981b06f56f01b3d042598dc23..f77e3cff3f2f3994b6137faed5e369f1ea33895d 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_access.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_access.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_access
 title: 'Test: Element: Access'
 description: 'Test element access controls'
@@ -33,14 +35,14 @@ elements: |
       '#type': textfield
       '#title': 'access_create_users (USER:1)'
       '#default_value': '{value}'
-      '#access_create_roles': { }
+      '#access_create_roles': {  }
       '#access_create_users':
         - 1
     access_create_permissions:
       '#type': textfield
       '#title': 'access_create_permissions (access user profiles)'
       '#default_value': '{value}'
-      '#access_create_roles': { }
+      '#access_create_roles': {  }
       '#access_create_permissions':
         - 'access user profiles'
   access_update:
@@ -63,14 +65,14 @@ elements: |
       '#type': textfield
       '#title': 'access_update_users (USER:1)'
       '#default_value': '{value}'
-      '#access_update_roles': { }
+      '#access_update_roles': {  }
       '#access_update_users':
         - 1
     access_update_permissions:
       '#type': textfield
       '#title': 'access_update_permissions (access user profiles)'
       '#default_value': '{value}'
-      '#access_update_roles': { }
+      '#access_update_roles': {  }
       '#access_update_permissions':
         - 'access user profiles'
   access_view:
@@ -93,14 +95,14 @@ elements: |
       '#type': textfield
       '#title': 'access_view_users (USER:1)'
       '#default_value': '{value}'
-      '#access_view_roles': { }
+      '#access_view_roles': {  }
       '#access_view_users':
         - 1
     access_view_permissions:
       '#type': textfield
       '#title': 'access_view_permissions (access user profiles)'
       '#default_value': '{value}'
-      '#access_view_roles': { }
+      '#access_view_roles': {  }
       '#access_view_permissions':
         - 'access user profiles'
 css: ''
@@ -111,6 +113,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -118,6 +121,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -133,22 +137,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -159,6 +178,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -177,9 +197,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -236,4 +258,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_actions.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_actions.yml
index 32f220442966cda226178f8d245d2e656ca4217a..d52aa476c571a885f3bef6483b876a39bfc1147d 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_actions.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_actions.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: 1
 template: false
+archive: false
 id: test_element_actions
 title: 'Test: Element: Actions'
 description: 'Test Actions (aka Submit button(s)) element.'
@@ -138,6 +140,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -145,6 +148,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -160,22 +164,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -186,6 +205,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: all
   draft_multiple: false
   draft_auto_save: false
@@ -204,9 +224,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -259,4 +281,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_actions_buttons.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_actions_buttons.yml
index 102772a8c5571727014bd840ec97f0cfb0662852..5e7dffc47584f7bc2b577bef52033366714ea2f2 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_actions_buttons.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_actions_buttons.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_actions_buttons
 title: 'Test: Element: Actions buttons'
 description: 'Test element actions buttons.'
@@ -60,6 +62,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -67,6 +70,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: true
   form_prepopulate_source_entity: false
@@ -82,22 +86,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -108,6 +127,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: authenticated
   draft_multiple: false
   draft_auto_save: false
@@ -126,9 +146,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -181,4 +203,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_address.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_address.yml
new file mode 100644
index 0000000000000000000000000000000000000000..22997548e7ee61ae38dbff1ef0653880bd4c46d7
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_address.yml
@@ -0,0 +1,234 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_address
+title: 'Test: Element: Address'
+description: 'Tests address element.'
+category: 'Test: Element'
+elements: |
+  address:
+    '#type': address
+    '#title': address_basic
+    '#default_value':
+      given_name: John
+      family_name: Smith
+      organization: 'Google Inc.'
+      address_line1: '1098 Alta Ave'
+      postal_code: '94043'
+      locality: 'Mountain View'
+      administrative_area: CA
+      country_code: US
+      langcode: en
+  address_advanced:
+    '#type': address
+    '#title': address_advanced
+    '#help': 'This is help text'
+    '#description': 'This is a description'
+    '#more_title': 'This is more title'
+    '#more': 'This is more text'
+    '#title_display': before
+    '#default_value':
+      organization: 'Google Inc.'
+      address_line1: '1098 Alta Ave'
+      postal_code: '94043'
+      locality: 'Mountain View'
+      administrative_area: CA
+      country_code: US
+      langcode: en
+    '#available_countries':
+      US: US
+    '#field_overrides':
+      givenName: hidden
+      additionalName: hidden
+      familyName: hidden
+      organization: hidden
+      sortingCode: hidden
+      dependentLocality: hidden
+  address_none:
+    '#type': address
+    '#title': address_none
+  address_multiple:
+    '#type': address
+    '#title': address_multiple
+    '#multiple': true
+    '#default_value':
+      - given_name: John
+        family_name: Smith
+        organization: 'Google Inc.'
+        address_line1: '1098 Alta Ave'
+        postal_code: '94043'
+        locality: 'Mountain View'
+        administrative_area: CA
+        country_code: US
+        langcode: en
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 1
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 1
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_allowed_tags.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_allowed_tags.yml
index 33428f198fba59fd57fb7dc8fdc21b93f7fde0f7..6a886c48481de05133ee1caed47038ed981a7828 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_allowed_tags.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_allowed_tags.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_allowed_tags
 title: 'Test: Element: Allowed tags'
 description: 'Test element #allowed_tags property.'
@@ -16,7 +18,7 @@ elements: |
   item:
     '#type': item
     '#title': 'Below markup contains HTML tags'
-    '#markup': 'Hello <ignored></tag><b>...Goodbye</b>'
+    '#markup': 'Hello <ignored></tag><b>…Goodbye</b>'
 css: ''
 javascript: ''
 settings:
@@ -25,6 +27,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -32,6 +35,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -47,22 +51,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -73,6 +92,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -91,9 +111,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -146,4 +168,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_attributes.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_attributes.yml
index a0d0a956ed6b64ca9c21873545c07b364c1f6dc7..e85c43f7df1bb6b76fbc3a1f7d8c105ef83980b1 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_attributes.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_attributes.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_attributes
 title: 'Test: Element: Attributes'
 description: 'Test options attributes.'
@@ -20,6 +22,7 @@ elements: |
       one
       two
       three
+      
     '#class__description': 'This is a custom class description.'
     '#style__description': 'This is a custom style description.'
     '#attributes__description': 'This is a custom attributes description.'
@@ -38,6 +41,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -45,6 +49,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -60,22 +65,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -86,6 +106,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -104,9 +125,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -159,6 +182,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_autocomplete.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_autocomplete.yml
index 033c99cae6c8df6faa47d0a9adebc80e7f8f7da8..b5e111b5ed90cd35c918ff7d427df21c2787806e 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_autocomplete.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_autocomplete.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_autocomplete
 title: 'Test: Element: Autocomplete'
 description: 'Test element autocompletion.'
@@ -16,7 +18,7 @@ elements: |
   autocomplete_off:
     '#type': email
     '#title': 'email (autocomplete=off)'
-    '#autocomplete': off
+    '#autocomplete': 'off'
   autocomplete_items:
     '#type': webform_autocomplete
     '#title': 'autocomplete (options)'
@@ -43,6 +45,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -50,6 +53,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -65,22 +69,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -91,6 +110,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -109,9 +129,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -164,4 +186,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_buttons.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_buttons.yml
index ca83785e6b3fc9d1423e0d1ae30ce4934d9acfc8..2e39e6c501141f6ace5da4afbfc3e27208b33d4e 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_buttons.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_buttons.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_buttons
 title: 'Test: Element: buttons'
 description: 'Test the buttons element.'
@@ -37,6 +39,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -44,6 +47,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -59,22 +63,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -85,6 +104,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -103,9 +123,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -158,6 +180,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_captcha.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_captcha.yml
index 36cc235df063ff07b20910c4d9b6cfb79f6760a6..f5e01999ab184c87274b422466b2a723ceb7bd28 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_captcha.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_captcha.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_captcha
 title: 'Test: Element: CAPTCHA'
 description: 'Tests CAPTCHA element.'
@@ -38,11 +40,6 @@ elements: |
   captcha_recaptcha:
     '#type': captcha
     '#captcha_type': recaptcha/reCAPTCHA
-  captcha_recaptcha:
-    '#type': captcha
-    '#captcha_type': recaptcha/reCAPTCHA
-    '#captcha_title': '{captcha_recaptcha}'
-    '#captcha_description': '{captcha_recaptcha}'
 css: ''
 javascript: ''
 settings:
@@ -51,6 +48,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -58,6 +56,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -73,22 +72,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -99,6 +113,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -117,9 +132,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -172,6 +189,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_checkbox_value.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_checkbox_value.yml
index cb6362eef8c6d8dab11e2f7e928672337f74b39a..4cf82f02946d734ad1ffbc149db4447cadb5715f 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_checkbox_value.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_checkbox_value.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_checkbox_value
 title: 'Test: Element: Checkbox value'
 description: 'Test the checkboxes value.'
@@ -22,11 +24,10 @@ elements: |
     '#title': checkbox_value_filled
     '#value__title': 'Enter a value'
     '#default_value': '{default_value}'
-  
   checkbox_value_select_other:
     '#type': webform_checkbox_value
     '#title': checkbox_value_select_other
-    '#default_value': 'Four'
+    '#default_value': Four
     '#element':
       '#type': webform_select_other
       '#options':
@@ -41,6 +42,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -48,6 +50,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -63,22 +66,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -89,6 +107,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -107,9 +126,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -162,6 +183,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_checkboxes.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_checkboxes.yml
index fcc6ca9fd499b15225b3014e24b3fade70fe0b7d..d9e1519eeaf57422cf17063ade03df707f8221a8 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_checkboxes.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_checkboxes.yml
@@ -6,13 +6,26 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_checkboxes
 title: 'Test: Element: Checkboxes'
 description: 'Test the checkboxes.'
 category: 'Test: Element'
 elements: |
+  checkbox_example:
+    '#type': details
+    '#title': Checkbox
+    '#open': true
+    checkbox:
+      '#type': checkbox
+      '#title': checkbox
+    checkbox_exclude_empty:
+      '#type': checkbox
+      '#title': checkbox_exclude_empty
+      '#exclude_empty': true
   checkboxes_descriptions_example:
     '#type': details
     '#title': 'Checkboxes descriptions'
@@ -81,6 +94,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -88,6 +102,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -103,22 +118,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -129,6 +159,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -147,9 +178,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -202,6 +235,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_codemirror.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_codemirror.yml
index bb4656a26c4b52e8219ee992a4b5bc492415716b..700c08136059cea3fe633f476eacbcda0ef631ed 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_codemirror.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_codemirror.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_codemirror
 title: 'Test: Element: CodeMirror'
 description: 'Tests CodeMirror element.'
@@ -18,7 +20,12 @@ elements: |
   text_basic:
     '#type': webform_codemirror
     '#title': text_basic
-    '#default_value': 'Hello'
+    '#default_value': Hello
+  text_basic_no_wrap:
+    '#type': webform_codemirror
+    '#title': text_basic_no_wrap
+    '#wrap': false
+    '#default_value': 'Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus,'
   yaml_basic:
     '#type': webform_codemirror
     '#mode': yaml
@@ -65,12 +72,13 @@ elements: |
           </script>
         </body>
       </html>
+      
   twig_basic:
     '#type': webform_codemirror
     '#mode': twig
     '#title': twig_basic
     '#default_value': |
-  
+      
       {% set value = "Hello" %}
       {{ value }}
 css: ''
@@ -81,6 +89,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -88,6 +97,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -103,22 +113,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -129,6 +154,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -147,9 +173,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -202,6 +230,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_composite.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_composite.yml
index 9d24504b32f6da69112190c05b3b1dcacb1320f2..1e73e92ea083ddd8a9604fbe3a770c1b99563b2a 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_composite.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_composite.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_composite
 title: 'Test: Element: Composite (Builder)'
 description: 'Test composite element builder used custom composites.'
@@ -51,7 +53,7 @@ elements: |
         '#title': 'Employment status'
       age:
         '#type': number
-        '#title': 'Age'
+        '#title': Age
         '#field_suffix': ' yrs. old'
         '#min': 1
         '#max': 125
@@ -63,6 +65,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -70,6 +73,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -85,22 +89,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -111,6 +130,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -129,9 +149,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -184,6 +206,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_composite_wrapper.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_composite_wrapper.yml
new file mode 100644
index 0000000000000000000000000000000000000000..46a4a143e04d34b96af71edecdce12cc9de09314
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_composite_wrapper.yml
@@ -0,0 +1,297 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_composite_wrapper
+title: 'Test: Element: Composite wrapper'
+description: 'Test composite element wrapper.'
+category: 'Test: Element'
+elements: |
+  radios_wrapper_fieldset:
+    '#type': radios
+    '#title': radios_wrapper_fieldset
+    '#required': true
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+  radios_wrapper_fieldset_hidden_title:
+    '#type': radios
+    '#title': radios_wrapper_fieldset_hidden_title
+    '#title_display': invisible
+    '#required': true
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+  radios_wrapper_fieldset_description:
+    '#type': radios
+    '#title': radios_wrapper_fieldset_description
+    '#required': true
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+    '#description': 'This is a description'
+  radios_wrapper_fieldset_element_descriptions:
+    '#type': radios
+    '#title': radios_wrapper_fieldset_element_descriptions
+    '#required': true
+    '#options':
+      One: 'One -- This is a radio description'
+      Two: 'Two -- This is a radio description'
+      Three: 'Three -- This is a radio description'
+    '#description': 'This is an element description'
+  checkboxes_wrapper_fieldset_description:
+    '#type': checkboxes
+    '#title': checkboxes_wrapper_fieldset_description
+    '#required': true
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+    '#description': 'This is a description'
+  checkboxes_wrapper_fieldset_element_descriptions:
+    '#type': checkboxes
+    '#title': checkboxes_wrapper_fieldset_element_descriptions
+    '#required': true
+    '#options':
+      One: 'One -- This is a checkbox description'
+      Two: 'Two -- This is a checkbox description'
+      Three: 'Three -- This is a checkbox description'
+    '#description': 'This is an element description'
+  select_other_wrapper_fieldset_description:
+    '#type': webform_select_other
+    '#title': select_other_wrapper_fieldset_description
+    '#required': true
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+    '#description': 'This is a description'
+  radios_wrapper_form_element:
+    '#type': radios
+    '#title': radios_wrapper_form_element
+    '#required': true
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+    '#wrapper_type': form_element
+  radios_wrapper_container:
+    '#type': radios
+    '#title': radios_wrapper_container
+    '#required': true
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+    '#wrapper_type': container
+  states_checkbox:
+    '#type': checkbox
+    '#title': states_checkbox
+  states_fieldset:
+    '#type': radios
+    '#title': states_fieldset
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+    '#states':
+      visible:
+        ':input[name="states_checkbox"]':
+          checked: true
+  states_form_item:
+    '#type': radios
+    '#title': states_form_item
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+    '#wrapper_type': form_element
+    '#states':
+      visible:
+        ':input[name="states_checkbox"]':
+          checked: true
+  states_container:
+    '#type': radios
+    '#title': states_container
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+    '#wrapper_type': container
+    '#states':
+      visible:
+        ':input[name="states_checkbox"]':
+          checked: true
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: true
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 1
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_computed_ajax.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_computed_ajax.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0db74973bbcea90523cdfa43d3863e93fc3ba525
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_computed_ajax.yml
@@ -0,0 +1,221 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_computed_ajax
+title: 'Test: Element: Computed ajax'
+description: 'Test computed element using Ajax.'
+category: 'Test: Element'
+elements: |
+  a:
+    '#type': webform_select_other
+    '#title': a
+    '#options':
+      1: 1
+      2: 2
+      3: 3
+    '#other__type': number
+  b:
+    '#type': number
+    '#title': b
+  webform_computed_token_a:
+    '#type': webform_computed_token
+    '#title': a
+    '#ajax': true
+    '#hide_empty': true
+    '#template': '[webform_submission:values:a:clear]'
+  webform_computed_token_b:
+    '#type': webform_computed_token
+    '#title': b
+    '#ajax': true
+    '#hide_empty': true
+    '#template': '[webform_submission:values:b:clear]'
+  webform_computed_twig:
+    '#type': webform_computed_twig
+    '#title': webform_computed_twig_data
+    '#ajax': true
+    '#template': |
+      {% spaceless %}
+      {% if data.a|length and data.b|length %}
+      {{ data.a }} + {{ data.b }} = {{ data.a + data.b }}
+      {% else %}
+      Please enter a value for a and b.
+      {% endif %}
+      {% endspaceless %}
+      
+  webform_computed_twig_token:
+    '#type': webform_computed_twig
+    '#title': webform_computed_twig_token
+    '#ajax': true
+    '#template': |
+      {% spaceless %}
+      {% set a = webform_token('[webform_submission:values:a:clear]', webform_submission) %}
+      {% set b = webform_token('[webform_submission:values:b:clear]', webform_submission) %}
+      {% if a|length and b|length %}
+      {{ a }} + {{ b }} = {{ a + b }}
+      {% else %}
+      Please enter a value for a and b.
+      {% endif %}
+      {% endspaceless %}
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 1
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_computed_token.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_computed_token.yml
index 8363ea223565b2f932267c54ce3a173fb11a1be1..99f205402e86878bed1c0c8410e5a4da5351b722 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_computed_token.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_computed_token.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_computed_token
 title: 'Test: Element: Computed token'
 description: 'Test computed token element.'
@@ -27,6 +29,7 @@ elements: |
     '#default_value': |
       <p>This is a <strong>text format</strong> string.</p>
       <p>It contains &quot;double&quot; and 'single' quotes with special characters like &lt;, &gt;, &lt;&gt;, and &gt;&lt;.</p>
+      
   xss:
     '#type': textfield
     '#title': xss
@@ -35,38 +38,41 @@ elements: |
     '#type': webform_computed_token
     '#title': webform_computed_token_auto
     '#display_on': view
-    '#value': |
+    '#template': |
       <b class="webform_computed_token_auto">simple string:</b> [webform_submission:values:simple_string]<br />
       <b class="webform_computed_token_auto">complex string :</b> [webform_submission:values:complex_string]<br />
       <b class="webform_computed_token_auto">text_format:</b> [webform_submission:values:text_format]<br />
       <b class="webform_computed_token_auto">xss:</b> [webform_submission:values:xss]<br />
+      
   webform_computed_token_html:
     '#type': webform_computed_token
     '#title': webform_computed_token_html
     '#mode': html
     '#display_on': view
-    '#value': |
+    '#template': |
       <b class="webform_computed_token_html">simple string:</b> [webform_submission:values:simple_string]<br />
       <b class="webform_computed_token_html">complex string :</b> [webform_submission:values:complex_string]<br />
       <b class="webform_computed_token_html">text_format:</b> [webform_submission:values:text_format]<br />
       <b class="webform_computed_token_html">xss:</b> [webform_submission:values:xss]<br />
+      
   webform_computed_token_text:
     '#type': webform_computed_token
     '#title': webform_computed_token_text
     '#mode': text
     '#display_on': view
-    '#value': |
+    '#template': |
       simple string: [webform_submission:values:simple_string]
       complex string : [webform_submission:values:complex_string]
       text_format: [webform_submission:values:text_format]
       xss: [webform_submission:values:xss]
+      
   webform_computed_token_store:
     '#type': webform_computed_token
     '#title': webform_computed_token_store
     '#mode': text
     '#display_on': none
-    '#value': '[webform_submission:values:simple_string]'
     '#store': true
+    '#template': 'sid: [webform_submission:sid]'
 css: ''
 javascript: ''
 settings:
@@ -75,6 +81,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -82,6 +89,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -97,22 +105,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -123,6 +146,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -141,9 +165,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -196,4 +222,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_computed_twig.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_computed_twig.yml
index a14b4b9c00941ce2fafcb54670ff3f110d918997..e334b4ba2294a0ed33600f38791826894f702bd1 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_computed_twig.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_computed_twig.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_computed_twig
 title: 'Test: Element: Computed twig'
 description: 'Test computed twig element.'
@@ -31,6 +33,7 @@ elements: |
     '#default_value': |
       <p>This is a <strong>text format</strong> string.</p>
       <p>It contains &quot;double&quot; and 'single' quotes with special characters like &lt;, &gt;, &lt;&gt;, and &gt;&lt;.</p>
+      
   xss:
     '#type': textfield
     '#title': xss
@@ -39,51 +42,65 @@ elements: |
     '#type': webform_computed_twig
     '#title': webform_computed_twig_auto
     '#display_on': view
-    '#value': |
+    '#template': |
       <b class="webform_computed_twig_auto">number:</b> {{ (webform_token('[webform_submission:values:number:clear]', webform_submission) ?: 0) }} * 2 = {{ (webform_token('[webform_submission:values:number:clear]', webform_submission) ?: 0) * 2 }}<br />
       <b class="webform_computed_twig_auto">simple string:</b> {{ webform_token('[webform_submission:values:simple_string]', webform_submission) }}<br />
       <b class="webform_computed_twig_auto">complex string:</b> {{ webform_token('[webform_submission:values:complex_string]', webform_submission) }}<br />
       <b class="webform_computed_twig_auto">text_format:</b> {{ webform_token('[webform_submission:values:text_format]', webform_submission) }}<br />
       <b class="webform_computed_twig_auto">xss:</b> {{ webform_token('[webform_submission:values:xss]', webform_submission) }}<br />
+      
   webform_computed_twig_html:
     '#type': webform_computed_twig
     '#title': webform_computed_twig_html
     '#mode': html
     '#display_on': view
-    '#value': |
+    '#template': |
       <b class="webform_computed_twig_html">number:</b> {{ (webform_token('[webform_submission:values:number:clear]', webform_submission) ?: 0) }} * 2 = {{ (webform_token('[webform_submission:values:number:clear]', webform_submission) ?: 0) * 2 }}<br />
       <b class="webform_computed_twig_html">simple string:</b> {{ webform_token('[webform_submission:values:simple_string]', webform_submission) }}<br />
       <b class="webform_computed_twig_html">complex string:</b> {{ webform_token('[webform_submission:values:complex_string]', webform_submission) }}<br />
       <b class="webform_computed_twig_html">text_format:</b> {{ webform_token('[webform_submission:values:text_format]', webform_submission) }}<br />
       <b class="webform_computed_twig_html">xss:</b> {{ webform_token('[webform_submission:values:xss]', webform_submission) }}<br />
+      
   webform_computed_twig_text:
     '#type': webform_computed_twig
     '#title': webform_computed_twig_text
     '#mode': text
     '#display_on': view
-    '#value': |
+    '#template': |
       number: {{ (webform_token('[webform_submission:values:number:clear]', webform_submission) ?: 0) }} * 2 = {{ (webform_token('[webform_submission:values:number:clear]', webform_submission) ?: 0) * 2 }}
       simple string: {{ webform_token('[webform_submission:values:simple_string]', webform_submission) }}
       complex string: {{ webform_token('[webform_submission:values:complex_string]', webform_submission) }}
       text_format: {{ webform_token('[webform_submission:values:text_format]', webform_submission) }}
       xss: {{ webform_token('[webform_submission:values:xss]', webform_submission) }}
+      
   webform_computed_twig_data:
     '#type': webform_computed_twig
     '#title': webform_computed_twig_data
     '#display_on': view
-    '#value': |
+    '#template': |
       <b class="webform_computed_twig_data">number:</b> {{ data.number }} * 2 = {{ data.number * 2 }}<br />
       <b class="webform_computed_twig_data">simple string:</b> {{ data.simple_string }}<br />
       <b class="webform_computed_twig_data">complex string:</b> {{ data.complex_string }}<br />
       <b class="webform_computed_twig_data">text_format:</b> {{ data.text_format.value }}<br />
       <b class="webform_computed_twig_data">xss:</b> {{ data.xss }}<br />
+      
   webform_computed_twig_store:
     '#type': webform_computed_twig
     '#title': webform_computed_twig_store
     '#mode': text
     '#display_on': none
-    '#value': '{{ data.simple_string }}'
     '#store': true
+    '#template': 'sid: {{ data.sid }}'
+  webform_computed_twig_trim:
+    '#type': webform_computed_twig
+    '#title': webform_computed_twig_trim
+    '#whitespace': trim
+    '#template': ' <em>This is trimmed</em>  <br/> '
+  webform_computed_twig_spaceless:
+    '#type': webform_computed_twig
+    '#title': webform_computed_twig_spaceless
+    '#whitespace': spaceless
+    '#template': ' <em>This is spaceless</em>  <br/> '
 css: ''
 javascript: ''
 settings:
@@ -92,6 +109,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -99,6 +117,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -114,22 +133,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -140,6 +174,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -158,9 +193,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -213,4 +250,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_container.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_container.yml
index de35002342ca84fda4755654f47633685d6d4b12..eb4568f991694d47352ec47ab9d47d74a02fe7cc 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_container.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_container.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_container
 title: 'Test: Element: Container'
 description: 'Test container elements.'
@@ -36,7 +38,7 @@ elements: |
     '#title': details_closed
     '#description': 'This is a details_closed description.'
     '#open': false
-    '#format': 'details-closed'
+    '#format': details-closed
     details_value:
       '#markup': 'This is a details_closed value.'
     details_textfield:
@@ -80,6 +82,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -87,6 +90,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -102,22 +106,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -128,6 +147,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -146,9 +166,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -201,4 +223,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_counter.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_counter.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ebab92f7191d6436d8400acd4298fcb3f5963275
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_counter.yml
@@ -0,0 +1,243 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_counter
+title: 'Test: Element: Counter'
+description: 'Tests text counter.'
+category: 'Test: Element'
+elements: |
+  counter_characters:
+    '#type': details
+    '#title': counter_characters
+    '#open': true
+    counter_characters_min:
+      '#type': textfield
+      '#title': 'counter_characters_min (5)'
+      '#counter_type': character
+      '#counter_minimum': 5
+    counter_characters_max:
+      '#type': textfield
+      '#title': 'counter_characters_max (10)'
+      '#counter_type': character
+      '#counter_maximum': 10
+    counter_characters_min_max:
+      '#type': textfield
+      '#title': 'counter_characters_min_max (5-10)'
+      '#counter_type': character
+      '#counter_minimum': 5
+      '#counter_maximum': 10
+    counter_characters_min_message:
+      '#type': textfield
+      '#title': 'counter_characters_min_message (5)'
+      '#counter_type': character
+      '#counter_minimum': 5
+      '#counter_minimum_message': '%d character(s) entered. This is custom text'
+    counter_characters_max_message:
+      '#type': textfield
+      '#title': 'counter_characters_max_message (10)'
+      '#counter_type': character
+      '#counter_maximum': 10
+      '#counter_maximum_message': '%d character(s) remaining. This is custom text'
+  counter_word:
+    '#type': details
+    '#title': counter_word
+    '#open': true
+    counter_words_min:
+      '#type': textarea
+      '#title': 'counter_words_min (5)'
+      '#counter_type': word
+      '#counter_minimum': 5
+    counter_words_max:
+      '#type': textarea
+      '#title': 'counter_words_max (10)'
+      '#counter_type': word
+      '#counter_maximum': 10
+    counter_words_min_max:
+      '#type': textarea
+      '#title': 'counter_words_min_max (5-10)'
+      '#counter_type': word
+      '#counter_minimum': 5
+      '#counter_maximum': 10
+    counter_words_min_message:
+      '#type': textarea
+      '#title': 'counter_words_min_message (5)'
+      '#counter_type': word
+      '#counter_minimum': 5
+      '#counter_minimum_message': '%d word(s) entered. This is custom text'
+    counter_words_max_message:
+      '#type': textarea
+      '#title': 'counter_words_max_message (10)'
+      '#counter_type': word
+      '#counter_maximum': 10
+      '#counter_maximum_message': '%d character(s) remaining. This is custom text'
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: true
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 1
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_date.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_date.yml
index ca0410fd5854ed6b31f6f2e7b8e64f59cd4b4d22..e185b0f377484cf7cc94f5af8bc57c9017e01ecc 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_date.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_date.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_date
 title: 'Test: Element: Date'
 description: 'Test date element.'
@@ -21,23 +23,23 @@ elements: |
     '#type': date
     '#title': date_custom
     '#date_date_format': d-M-Y
-    '#min': '2008-01-01'
-    '#max': '2010-12-31'
+    '#date_date_min': '2008-01-01'
+    '#date_date_max': '2010-12-31'
     '#default_value': '2009-08-18'
     '#description': '(2008-01-01 to 2010-12-31) + (d-M-Y)'
   date_min_max:
     '#type': date
     '#title': date_min_max
     '#description': '(2009-01-01 to 2009-12-31)'
-    '#min': '2009-01-01'
-    '#max': '2009-12-31'
+    '#date_date_min': '2009-01-01'
+    '#date_date_max': '2009-12-31'
     '#default_value': '2009-08-18'
   date_min_max_dynamic:
     '#type': date
     '#title': date_min_max_dynamic
     '#description': '(-1 year to +1 year)'
-    '#min': '-1 year'
-    '#max': '+1 year'
+    '#date_date_min': '-1 year'
+    '#date_date_max': '+1 year'
     '#default_value': now
   date_datepicker:
     '#type': date
@@ -45,6 +47,13 @@ elements: |
     '#datepicker': true
     '#date_date_format': 'D, m/d/Y'
     '#default_value': '2009-08-18'
+  date_datepicker_button:
+    '#type': date
+    '#title': date_datepicker_button
+    '#datepicker': true
+    '#datepicker_button': true
+    '#date_date_format': 'D, m/d/Y'
+    '#default_value': '2009-08-18'
   date_datepicker_custom:
     '#type': date
     '#title': date_datepicker_custom
@@ -57,8 +66,8 @@ elements: |
     '#datepicker': true
     '#date_date_format': 'D, m/d/Y'
     '#description': '(-1 year to +1 year)'
-    '#min': '-1 year'
-    '#max': '+1 year'
+    '#date_date_min': '-1 year'
+    '#date_date_max': '+1 year'
     '#default_value': now
 css: ''
 javascript: ''
@@ -68,6 +77,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -75,6 +85,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -90,22 +101,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -116,6 +142,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -134,9 +161,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -189,4 +218,16 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
-handlers: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 1
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_datelist.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_datelist.yml
index 378c63663f881c6e8b3a0ae7131278ac24df189e..517b21c771e4eee168f725e24a09fa77082b9cbd 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_datelist.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_datelist.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_datelist
 title: 'Test: Element: Date list'
 description: 'Test date list element.'
@@ -17,6 +19,23 @@ elements: |
     '#type': datelist
     '#title': datelist_default
     '#default_value': '2009-08-18T01:00:00-05:00'
+  datelist_no_abbreviate:
+    '#type': datelist
+    '#title': datelist_no_abbreviate
+    '#default_value': '2009-08-18T01:00:00-05:00'
+    '#date_abbreviate': false
+  datelist_text_parts:
+    '#type': datelist
+    '#title': datelist_text_parts
+    '#default_value': '2009-08-18T01:00:00-05:00'
+    '#date_text_parts':
+      - month
+      - day
+      - year
+      - hour
+      - minute
+      - second
+      - ampm
   datelist_datetime:
     '#type': datelist
     '#title': datelist_datetime
@@ -41,20 +60,41 @@ elements: |
     '#type': datelist
     '#title': datelist_min_max
     '#description': '(2009-01-01 to 2009-12-31)'
-    '#min': '2009-01-01'
-    '#max': '2009-12-31'
+    '#date_date_min': '2009-01-01'
+    '#date_date_max': '2009-12-31'
     '#default_value': '2009-08-18'
+  datelist_min_max_time:
+    '#type': datelist
+    '#title': datelist_min_max_time
+    '#description': '(2009-01-01 09:00:00 to 2009-12-31 17:00:00)'
+    '#date_min': '2009-01-01 09:00:00'
+    '#date_max': '2009-12-31 17:00:00'
+    '#default_value': '2009-01-01 09:00:00'
   datelist_date_year_range_reverse:
     '#type': datelist
     '#title': datelist_date_year_range_reverse
     '#date_year_range': '2005:2010'
-    '#date_year_range_reverse': TRUE
+    '#date_year_range_reverse': true
   datelist_required_error:
     '#type': datelist
     '#title': datelist_required_error
     '#default_value': '2009-08-18T01:00:00-05:00'
     '#required': 'Custom required error'
     '#required_error': 'Custom required error'
+  datelist_multiple:
+    '#type': datelist
+    '#title': datelist_multiple
+    '#default_value': '2009-08-18T01:00:00-05:00'
+    '#multiple': true
+  datelist_custom_composite:
+    '#type': webform_custom_composite
+    '#title': datelist_custom_composite
+    '#element':
+      datelist:
+        '#type': datelist
+        '#title': datelist
+    '#default_value':
+      - datelist: '2009-08-18T01:00:00-05:00'
 css: ''
 javascript: ''
 settings:
@@ -63,6 +103,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -70,6 +111,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -85,22 +127,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -111,6 +168,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -129,9 +187,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -184,4 +244,16 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
-handlers: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 1
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_datetime.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_datetime.yml
index 98a2a3189bc80efb6dc7cfe509f4e1c1c4715bab..483106b105ec739477b15f7a8c783a2741775302 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_datetime.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_datetime.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_datetime
 title: 'Test: Element: Date/time'
 description: 'Test date/time element'
@@ -45,16 +47,23 @@ elements: |
   datetime_year_range:
     '#type': datetime
     '#title': datetime_year_range
-    '#descripion': '(-10:+1)'
+    '#description': '(-10:+1)'
     '#default_value': '2009-08-18T01:00:00-05:00'
     '#date_year_range': '-10:+1'
   datetime_min_max:
     '#type': datetime
     '#title': datetime_min_max
     '#description': '(2009-01-01 to 2009-12-31)'
-    '#min': '2009-01-01'
-    '#max': '2009-12-31'
+    '#date_date_min': '2009-01-01'
+    '#date_date_max': '2009-12-31'
     '#default_value': '2009-08-18'
+  datetime_min_max_time:
+    '#type': datetime
+    '#title': datetime_min_max_time
+    '#description': '(2009-01-01 09:00:00 to 2009-12-31 17:00:00)'
+    '#date_min': '2009-01-01 09:00:00'
+    '#date_max': '2009-12-31 17:00:00'
+    '#default_value': '2009-01-01 09:00:00'
   datetime_datepicker_timepicker:
     '#type': datetime
     '#title': datetime_datepicker_timepicker
@@ -63,12 +72,26 @@ elements: |
     '#date_date_format': 'D, m/d/Y'
     '#date_time_element': timepicker
     '#date_time_format': 'g:i A'
+  datetime_datepicker_time_text:
+    '#type': datetime
+    '#title': datetime_datepicker_time_text
+    '#default_value': '2009-08-18T01:00:00-05:00'
+    '#date_time_element': text
+  datetime_datepicker_timepicker_button:
+    '#type': datetime
+    '#title': datetime_datepicker_timepicker_button
+    '#default_value': '2009-08-18T01:00:00-05:00'
+    '#date_date_element': datepicker
+    '#date_date_datepicker_button': true
+    '#date_date_format': 'D, m/d/Y'
+    '#date_time_element': timepicker
+    '#date_time_format': 'g:i A'
   datetime_time_min_max:
     '#type': datetime
     '#title': datetime_time_min_max
     '#description': '(2009-01-01 to 2009-12-31 and 09:00:00 to 17:00:00)'
-    '#min': '2009-01-01'
-    '#max': '2009-12-31'
+    '#date_date_min': '2009-01-01'
+    '#date_date_max': '2009-12-31'
     '#date_time_min': '09:00:00'
     '#date_time_max': '17:00:00'
     '#date_time_step': '1800'
@@ -77,8 +100,8 @@ elements: |
     '#type': datetime
     '#title': datetime_datepicker_timepicker_time_min_max
     '#description': '(2009-01-01 to 2009-12-31 and 09:00:00 to 17:00:00)'
-    '#min': '2009-01-01'
-    '#max': '2009-12-31'
+    '#date_date_min': '2009-01-01'
+    '#date_date_max': '2009-12-31'
     '#date_time_min': '09:00:00'
     '#date_time_max': '17:00:00'
     '#date_time_step': '1800'
@@ -91,13 +114,38 @@ elements: |
     '#type': datetime
     '#title': datetime_no_seconds
     '#description': |
-      @see <a href="https://www.drupal.org/node/2917107">Issue #2917107: Date and Time validation problem</a><b r/>
-      @see <a href="https://www.drupal.org/node/2723159">Issue #2723159: Datetime form element cannot validate when using a format without seconds</a><b r/>
+      @see <a href="https://www.drupal.org/node/2917107">Issue #2917107: Date and Time validation problem</a><br/>
+      @see <a href="https://www.drupal.org/node/2723159">Issue #2723159: Datetime form element cannot validate when using a format without seconds</a><br/>
+      
     '#date_date_element': datepicker
     '#date_time_element': text
     '#date_time_format': 'H:i'
     '#date_time_step': '900'
     '#default_value': '2009-08-18T01:00:00-05:00'
+  datetime_multiple:
+    '#type': datetime
+    '#title': datetime_multiple
+    '#default_value': '2009-08-18T01:00:00-05:00'
+    '#date_date_element': datepicker
+    '#date_date_format': 'D, m/d/Y'
+    '#date_time_element': timepicker
+    '#date_time_format': 'g:i A'
+    '#multiple': true
+  datetime_custom_composite:
+    '#type': webform_custom_composite
+    '#title': datetime_custom_composite
+    '#element':
+      datetime:
+        '#type': datetime
+        '#title': datetime
+        '#date_date_element': datepicker
+        '#date_date_format': d/m/Y
+        '#date_time_element': timepicker
+        '#date_time_format': 'g:i A'
+        '#date_time_step': '900'
+        '#autocomplete': 'off'
+    '#default_value':
+      - datetime: '2009-08-18T01:00:00-05:00'
 css: ''
 javascript: ''
 settings:
@@ -106,6 +154,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -113,6 +162,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -128,22 +178,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -154,6 +219,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -172,9 +238,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -227,4 +295,16 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
-handlers: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 1
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_description_tooltip.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_description_tooltip.yml
index e7c43d746b410663015df5cf038765a4ac7ecc9c..949bd917f129db63c51a8d9a6af9daea9e5c3c9c 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_description_tooltip.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_description_tooltip.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_description_tooltip
 title: 'Test: Element: Description tooltip'
 description: 'Test element eescription tooltip.'
@@ -105,7 +107,7 @@ elements: |
       '#min': 0
       '#max': 100
       '#step': 1
-      '#output': right
+      '#output': below
       '#output__field_prefix': $
       '#output__field_suffix': '.00'
       '#description': 'This is a description for the ''range'' element.'
@@ -141,18 +143,52 @@ elements: |
       '#title': URL
       '#description': 'This is a description for the ''url'' element.'
       '#description_display': tooltip
+  file_upload_elements:
+    '#type': details
+    '#title': 'File upload elements'
+    '#open': true
+    webform_audio_file:
+      '#type': webform_audio_file
+      '#title': 'Audio file'
+      '#description': 'This is a description for the ''webform_audio_file'' element.'
+      '#description_display': tooltip
+    webform_document_file:
+      '#type': webform_document_file
+      '#title': 'Document file'
+      '#description': 'This is a description for the ''webform_document_file'' element.'
+      '#description_display': tooltip
+    managed_file:
+      '#type': managed_file
+      '#title': File
+      '#description': 'This is a description for the ''managed_file'' element.'
+      '#description_display': tooltip
+    webform_image_file:
+      '#type': webform_image_file
+      '#title': 'Image file'
+      '#description': 'This is a description for the ''webform_image_file'' element.'
+      '#description_display': tooltip
+    webform_video_file:
+      '#type': webform_video_file
+      '#title': 'Video file'
+      '#description': 'This is a description for the ''webform_video_file'' element.'
+      '#description_display': tooltip
   composite_elements:
     '#type': details
     '#title': 'Composite elements'
     '#open': true
     webform_address:
       '#type': webform_address
-      '#title': Address
+      '#title': 'Basic address'
       '#description': 'This is a description for the ''webform_address'' element.'
       '#description_display': tooltip
+    webform_contact:
+      '#type': webform_contact
+      '#title': Contact
+      '#description': 'This is a description for the ''webform_contact'' element.'
+      '#description_display': tooltip
     webform_custom_composite:
       '#type': webform_custom_composite
-      '#title': 'Composite custom'
+      '#title': 'Custom composite'
       '#element':
         name:
           '#type': textfield
@@ -165,25 +201,20 @@ elements: |
           '#options':
             Male: Male
             Female: Female
-      '#description': 'This is a description for the ''webform_composite'' element.'
-      '#description_display': tooltip
-    webform_contact:
-      '#type': webform_contact
-      '#title': Contact
-      '#description': 'This is a description for the ''webform_contact'' element.'
+      '#description': 'This is a description for the ''webform_custom_composite'' element.'
       '#description_display': tooltip
     webform_link:
       '#type': webform_link
       '#title': Link
       '#description': 'This is a description for the ''webform_link'' element.'
       '#description_display': tooltip
-    webform_location:
-      '#type': webform_location
+    webform_location_geocomplete:
+      '#type': webform_location_places
       '#title': Location
       '#map': true
       '#geolocation': true
       '#format': map
-      '#description': 'This is a description for the ''webform_location'' element.'
+      '#description': 'This is a description for the ''webform_location_geocomplete'' element.'
       '#description_display': tooltip
     webform_name:
       '#type': webform_name
@@ -195,35 +226,6 @@ elements: |
       '#title': 'Telephone advanced'
       '#description': 'This is a description for the ''webform_telephone'' element.'
       '#description_display': tooltip
-  file_upload_elements:
-    '#type': details
-    '#title': 'File upload elements'
-    '#open': true
-    webform_audio_file:
-      '#type': webform_audio_file
-      '#title': 'Audio file'
-      '#description': 'This is a description for the ''webform_audio_file'' element.'
-      '#description_display': tooltip
-    webform_document_file:
-      '#type': webform_document_file
-      '#title': 'Document file'
-      '#description': 'This is a description for the ''webform_document_file'' element.'
-      '#description_display': tooltip
-    managed_file:
-      '#type': managed_file
-      '#title': File
-      '#description': 'This is a description for the ''managed_file'' element.'
-      '#description_display': tooltip
-    webform_image_file:
-      '#type': webform_image_file
-      '#title': 'Image file'
-      '#description': 'This is a description for the ''webform_image_file'' element.'
-      '#description_display': tooltip
-    webform_video_file:
-      '#type': webform_video_file
-      '#title': 'Video file'
-      '#description': 'This is a description for the ''webform_video_file'' element.'
-      '#description_display': tooltip
   options_elements:
     '#type': details
     '#title': 'Options elements'
@@ -349,15 +351,15 @@ elements: |
     webform_computed_token:
       '#type': webform_computed_token
       '#title': 'Computed token'
-      '#value': 'This is a Computed token value.'
       '#description': 'This is a description for the ''webform_computed_token'' element.'
       '#description_display': tooltip
+      '#template': 'This is a Computed token value.'
     webform_computed_twig:
       '#type': webform_computed_twig
       '#title': 'Computed Twig'
-      '#value': 'This is a Computed Twig value.'
       '#description': 'This is a description for the ''webform_computed_twig'' element.'
       '#description_display': tooltip
+      '#template': 'This is a Computed Twig value.'
   date_time_elements:
     '#type': details
     '#title': 'Date/time elements'
@@ -479,6 +481,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -486,6 +489,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -501,22 +505,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -527,6 +546,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -545,9 +565,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -600,4 +622,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_details.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_details.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0d71c8c39a3b55fe5e4899afbc0946ee129997d2
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_details.yml
@@ -0,0 +1,202 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_details
+title: 'Test: Element: Details'
+description: 'Test the details element.'
+category: 'Test: Element'
+elements: |
+  details:
+    '#type': details
+    '#title': details
+    '#description': 'This is a description.'
+    '#help': 'This is help text.'
+    '#more': 'This is more text'
+    '#required': true
+    '#open': true
+    details_markup:
+      '#markup': '<p>This is some markup</p>'
+  details_title_invisible:
+    '#type': details
+    '#title': 'Details title invisible'
+    '#description': 'This is a description.'
+    '#title_display': invisible
+    details_title_invisible_markup:
+      '#markup': '<p>This is some markup</p>'
+  details_more:
+    '#type': details
+    '#title': 'details more'
+    '#more': 'This is more text'
+    '#open': true
+    details_more_markup:
+      '#markup': '<p>This is some markup</p>'
+  details_help:
+    '#type': details
+    '#title': 'details help'
+    '#help': 'This is help text.'
+    '#open': true
+    details_help_markup:
+      '#markup': '<p>This is some markup</p>'
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_disabled.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_disabled.yml
index a31788cf303c1eed23edea64c683b3005808bcec..12cb8f4ce322db3d0a8366f50fdc66ef1536ed75 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_disabled.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_disabled.yml
@@ -6,17 +6,15 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_disabled
 title: 'Test: Element disabled'
 description: 'Test disabling element.'
 category: 'Test: Element'
 elements: |
-  webform_address:
-    '#type': webform_address
-    '#title': Address
-    '#disabled': true
   webform_audio_file:
     '#type': webform_audio_file
     '#title': 'Audio file'
@@ -25,6 +23,10 @@ elements: |
     '#type': webform_autocomplete
     '#title': Autocomplete
     '#disabled': true
+  webform_address:
+    '#type': webform_address
+    '#title': 'Basic address'
+    '#disabled': true
   webform_buttons:
     '#type': webform_buttons
     '#title': Buttons
@@ -73,9 +75,13 @@ elements: |
     '#type': color
     '#title': Color
     '#disabled': true
+  webform_contact:
+    '#type': webform_contact
+    '#title': Contact
+    '#disabled': true
   webform_custom_composite:
     '#type': webform_custom_composite
-    '#title': 'Composite custom'
+    '#title': 'Custom composite'
     '#element':
       name:
         '#type': textfield
@@ -89,10 +95,6 @@ elements: |
           Male: Male
           Female: Female
     '#disabled': true
-  webform_contact:
-    '#type': webform_contact
-    '#title': Contact
-    '#disabled': true
   date:
     '#type': date
     '#title': Date
@@ -195,8 +197,8 @@ elements: |
     '#type': webform_link
     '#title': Link
     '#disabled': true
-  webform_location:
-    '#type': webform_location
+  webform_location_geocomplete:
+    '#type': webform_location_places
     '#title': Location
     '#map': true
     '#geolocation': true
@@ -263,7 +265,7 @@ elements: |
     '#min': 0
     '#max': 100
     '#step': 1
-    '#output': right
+    '#output': below
     '#output__field_prefix': $
     '#output__field_suffix': '.00'
     '#disabled': true
@@ -384,6 +386,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -391,6 +394,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -406,22 +410,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -432,6 +451,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -450,9 +470,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -505,4 +527,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_email.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_email.yml
index e5696d95d681f0295efaf88649a2017571e9626b..701ef805dc26d23814806f277cfcb1597d2e339e 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_email.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_email.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_email
 title: 'Test: Element: Email'
 description: 'Tests email_confirm and email_multiple elements.'
@@ -58,6 +60,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -65,6 +68,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -80,22 +84,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -106,6 +125,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -124,9 +144,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -179,6 +201,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_entity_reference.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_entity_reference.yml
index 747041f8ad0c435eb2931999bd6546228f2adeac..9eb120f994e3638609e266c388e7014784422e3b 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_entity_reference.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_entity_reference.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_entity_reference
 title: 'Test: Element: Entity reference'
 description: 'Test entity reference elements.'
@@ -122,6 +124,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -129,6 +132,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -144,22 +148,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -170,6 +189,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -188,9 +208,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -243,6 +265,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_fieldset.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_fieldset.yml
new file mode 100644
index 0000000000000000000000000000000000000000..caec13c52e1a91f5e0cb64245ec4b32328f5d574
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_fieldset.yml
@@ -0,0 +1,211 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_fieldset
+title: 'Test: Element: Fieldset'
+description: 'Test the fieldset element.'
+category: 'Test: Element'
+elements: |
+  fieldset:
+    '#type': fieldset
+    '#title': fieldset
+    '#description': 'This is a description.'
+    '#help': 'This is help text.'
+    '#more': 'This is more text'
+    '#required': true
+    '#field_prefix': prefix
+    '#field_suffix': suffix
+    fieldset_markup:
+      '#markup': '<p>This is some markup</p>'
+  fieldset_title_invisible:
+    '#type': fieldset
+    '#title': fieldset_title_invisible
+    '#description': 'This is a description.'
+    '#title_display': invisible
+  fieldset_description_before:
+    '#type': fieldset
+    '#title': fieldset_description_before
+    '#description': 'This is a description before.'
+    '#description_display': before
+    '#help': 'This is help text.'
+    '#more': 'This is more text'
+    '#required': true
+    '#field_prefix': prefix
+    '#field_suffix': suffix
+    fieldset_markup:
+      '#markup': '<p>This is some markup</p>'
+  fieldset_description_tooltip:
+    '#type': fieldset
+    '#title': fieldset_description_tooltip
+    '#description': 'This is a description tooltip.'
+    '#description_display': tooltip
+    '#help': 'This is help text.'
+    '#more': 'This is more text'
+    '#required': true
+    '#field_prefix': prefix
+    '#field_suffix': suffix
+    fieldset_markup:
+      '#markup': '<p>This is some markup</p>'
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_flexbox.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_flexbox.yml
index e7ba4c021757f5334cfda13da3a29ffce1b27717..9f20ad856f04ec93f93dd2db893594eefba83cc5 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_flexbox.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_flexbox.yml
@@ -6,31 +6,15 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_flexbox
 title: 'Test: Element: Flexbox'
 description: 'Test flexbox layout.'
 category: 'Test: Element'
 elements: |
-  flexbox_processed_text:
-    '#type': webform_flexbox
-    flex_processed_text_left:
-      '#type': processed_text
-      '#title': 'Advanced HTML/Text 01'
-    flex_processed_text_right:
-      '#type': processed_text
-      '#title': 'Advanced HTML/Text 02'
-  flexbox_webform_markup:
-    '#type': webform_flexbox
-    flex_webform_markup_left:
-      '#type': webform_markup
-      '#title': 'Basic HTML 01'
-      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/2700667">Issue #2700667: Notice: Undefined index: #type in drupal_process_states()</a>'
-    flex_webform_markup_right:
-      '#type': webform_markup
-      '#title': 'Basic HTML 02'
-      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/2700667">Issue #2700667: Notice: Undefined index: #type in drupal_process_states()</a>'
   flexbox_container:
     '#type': webform_flexbox
     flex_container_left:
@@ -57,44 +41,6 @@ elements: |
     flex_fieldset_right:
       '#type': fieldset
       '#title': 'Fieldset 02'
-  flexbox_item:
-    '#type': webform_flexbox
-    flex_item_left:
-      '#type': item
-      '#title': 'Item 01'
-      '#markup': '{markup}'
-      '#field_prefix': '{field_prefix}'
-      '#field_suffix': '{field_suffix}'
-      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/783438">Issue #783438: #states doesn''t work for #type item</a>'
-    flex_item_right:
-      '#type': item
-      '#title': 'Item 02'
-      '#markup': '{markup}'
-      '#field_prefix': '{field_prefix}'
-      '#field_suffix': '{field_suffix}'
-      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/783438">Issue #783438: #states doesn''t work for #type item</a>'
-  flexbox_label:
-    '#type': webform_flexbox
-    flex_label_left:
-      '#type': label
-      '#title': 'Label 01'
-    flex_label_right:
-      '#type': label
-      '#title': 'Label 02'
-  flexbox_webform_message:
-    '#type': webform_flexbox
-    flex_webform_message_left:
-      '#type': webform_message
-      '#title': 'Message 01'
-      '#message_type': warning
-      '#message_message': 'This is a <strong>warning</strong> message.'
-      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/77245">Issue #77245: A place for JavaScript status messages</a>'
-    flex_webform_message_right:
-      '#type': webform_message
-      '#title': 'Message 02'
-      '#message_type': warning
-      '#message_message': 'This is a <strong>warning</strong> message.'
-      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/77245">Issue #77245: A place for JavaScript status messages</a>'
   flexbox_webform_section:
     '#type': webform_flexbox
     flex_webform_section_left:
@@ -111,14 +57,22 @@ elements: |
     flex_table_right:
       '#type': table
       '#title': 'Table 02'
-  flexbox_webform_address:
+  flexbox_address:
+    '#type': webform_flexbox
+    flex_address_left:
+      '#type': address
+      '#title': 'Advanced address 01'
+    flex_address_right:
+      '#type': address
+      '#title': 'Advanced address 02'
+  flexbox_processed_text:
     '#type': webform_flexbox
-    flex_webform_address_left:
-      '#type': webform_address
-      '#title': 'Address 01'
-    flex_webform_address_right:
-      '#type': webform_address
-      '#title': 'Address 02'
+    flex_processed_text_left:
+      '#type': processed_text
+      '#title': 'Advanced HTML/Text 01'
+    flex_processed_text_right:
+      '#type': processed_text
+      '#title': 'Advanced HTML/Text 02'
   flexbox_webform_audio_file:
     '#type': webform_flexbox
     flex_webform_audio_file_left:
@@ -135,6 +89,24 @@ elements: |
     flex_webform_autocomplete_right:
       '#type': webform_autocomplete
       '#title': 'Autocomplete 02'
+  flexbox_webform_address:
+    '#type': webform_flexbox
+    flex_webform_address_left:
+      '#type': webform_address
+      '#title': 'Basic address 01'
+    flex_webform_address_right:
+      '#type': webform_address
+      '#title': 'Basic address 02'
+  flexbox_webform_markup:
+    '#type': webform_flexbox
+    flex_webform_markup_left:
+      '#type': webform_markup
+      '#title': 'Basic HTML 01'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/2700667">Issue #2700667: Notice: Undefined index: #type in drupal_process_states()</a>'
+    flex_webform_markup_right:
+      '#type': webform_markup
+      '#title': 'Basic HTML 02'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/2700667">Issue #2700667: Notice: Undefined index: #type in drupal_process_states()</a>'
   flexbox_webform_buttons:
     '#type': webform_flexbox
     flex_webform_buttons_left:
@@ -243,11 +215,39 @@ elements: |
     flex_color_right:
       '#type': color
       '#title': 'Color 02'
-  flexbox_webform_composite:
+  flexbox_webform_computed_token:
     '#type': webform_flexbox
-    flex_webform_composite_left:
+    flex_webform_computed_token_left:
+      '#type': webform_computed_token
+      '#title': 'Computed token 01'
+      '#template': 'This is a Computed token value.'
+    flex_webform_computed_token_right:
+      '#type': webform_computed_token
+      '#title': 'Computed token 02'
+      '#template': 'This is a Computed token value.'
+  flexbox_webform_computed_twig:
+    '#type': webform_flexbox
+    flex_webform_computed_twig_left:
+      '#type': webform_computed_twig
+      '#title': 'Computed Twig 01'
+      '#template': 'This is a Computed Twig value.'
+    flex_webform_computed_twig_right:
+      '#type': webform_computed_twig
+      '#title': 'Computed Twig 02'
+      '#template': 'This is a Computed Twig value.'
+  flexbox_webform_contact:
+    '#type': webform_flexbox
+    flex_webform_contact_left:
+      '#type': webform_contact
+      '#title': 'Contact 01'
+    flex_webform_contact_right:
+      '#type': webform_contact
+      '#title': 'Contact 02'
+  flexbox_webform_custom_composite:
+    '#type': webform_flexbox
+    flex_webform_custom_composite_left:
       '#type': webform_custom_composite
-      '#title': 'Composite custom 01'
+      '#title': 'Custom composite 01'
       '#element':
         name:
           '#type': textfield
@@ -260,9 +260,9 @@ elements: |
           '#options':
             Male: Male
             Female: Female
-    flex_webform_composite_right:
+    flex_webform_custom_composite_right:
       '#type': webform_custom_composite
-      '#title': 'Composite custom 02'
+      '#title': 'Custom composite 02'
       '#element':
         name:
           '#type': textfield
@@ -275,34 +275,6 @@ elements: |
           '#options':
             Male: Male
             Female: Female
-  flexbox_webform_computed_token:
-    '#type': webform_flexbox
-    flex_webform_computed_token_left:
-      '#type': webform_computed_token
-      '#title': 'Computed token 01'
-      '#value': 'This is a Computed token value.'
-    flex_webform_computed_token_right:
-      '#type': webform_computed_token
-      '#title': 'Computed token 02'
-      '#value': 'This is a Computed token value.'
-  flexbox_webform_computed_twig:
-    '#type': webform_flexbox
-    flex_webform_computed_twig_left:
-      '#type': webform_computed_twig
-      '#title': 'Computed Twig 01'
-      '#value': 'This is a Computed Twig value.'
-    flex_webform_computed_twig_right:
-      '#type': webform_computed_twig
-      '#title': 'Computed Twig 02'
-      '#value': 'This is a Computed Twig value.'
-  flexbox_webform_contact:
-    '#type': webform_flexbox
-    flex_webform_contact_left:
-      '#type': webform_contact
-      '#title': 'Contact 01'
-    flex_webform_contact_right:
-      '#type': webform_contact
-      '#title': 'Contact 02'
   flexbox_date:
     '#type': webform_flexbox
     flex_date_left:
@@ -467,14 +439,12 @@ elements: |
         class:
           - webform-horizontal-rule--dotted
           - webform-horizontal-rule--thick
-      '#title': 'webform_horizontal_rule 01'
     flex_webform_horizontal_rule_right:
       '#type': webform_horizontal_rule
       '#attributes':
         class:
           - webform-horizontal-rule--dotted
           - webform-horizontal-rule--thick
-      '#title': 'webform_horizontal_rule 02'
   flexbox_webform_image_file:
     '#type': webform_flexbox
     flex_webform_image_file_left:
@@ -513,6 +483,30 @@ elements: |
         bear_3:
           text: 'Bear 3'
           src: 'https://www.placebear.com/120/100'
+  flexbox_item:
+    '#type': webform_flexbox
+    flex_item_left:
+      '#type': item
+      '#title': 'Item 01'
+      '#markup': '{markup}'
+      '#field_prefix': '{field_prefix}'
+      '#field_suffix': '{field_suffix}'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/783438">Issue #783438: #states doesn''t work for #type item</a>'
+    flex_item_right:
+      '#type': item
+      '#title': 'Item 02'
+      '#markup': '{markup}'
+      '#field_prefix': '{field_prefix}'
+      '#field_suffix': '{field_suffix}'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/783438">Issue #783438: #states doesn''t work for #type item</a>'
+  flexbox_label:
+    '#type': webform_flexbox
+    flex_label_left:
+      '#type': label
+      '#title': 'Label 01'
+    flex_label_right:
+      '#type': label
+      '#title': 'Label 02'
   flexbox_language_select:
     '#type': webform_flexbox
     flex_language_select_left:
@@ -553,16 +547,16 @@ elements: |
     flex_webform_link_right:
       '#type': webform_link
       '#title': 'Link 02'
-  flexbox_webform_location:
+  flexbox_webform_location_places:
     '#type': webform_flexbox
-    flex_webform_location_left:
-      '#type': webform_location
+    flex_webform_location_places_left:
+      '#type': webform_location_places
       '#title': 'Location 01'
       '#map': true
       '#geolocation': true
       '#format': map
-    flex_webform_location_right:
-      '#type': webform_location
+    flex_webform_location_places_right:
+      '#type': webform_location_places
       '#title': 'Location 02'
       '#map': true
       '#geolocation': true
@@ -599,6 +593,20 @@ elements: |
         four: Four
         five: Five
         six: Six
+  flexbox_webform_message:
+    '#type': webform_flexbox
+    flex_webform_message_left:
+      '#type': webform_message
+      '#title': 'Message 01'
+      '#message_type': warning
+      '#message_message': 'This is a <strong>warning</strong> message.'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/77245">Issue #77245: A place for JavaScript status messages</a>'
+    flex_webform_message_right:
+      '#type': webform_message
+      '#title': 'Message 02'
+      '#message_type': warning
+      '#message_message': 'This is a <strong>warning</strong> message.'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/77245">Issue #77245: A place for JavaScript status messages</a>'
   flexbox_webform_name:
     '#type': webform_flexbox
     flex_webform_name_left:
@@ -685,7 +693,7 @@ elements: |
       '#min': 0
       '#max': 100
       '#step': 1
-      '#output': right
+      '#output': below
       '#output__field_prefix': $
       '#output__field_suffix': '.00'
     flex_range_right:
@@ -694,7 +702,7 @@ elements: |
       '#min': 0
       '#max': 100
       '#step': 1
-      '#output': right
+      '#output': below
       '#output__field_prefix': $
       '#output__field_suffix': '.00'
   flexbox_webform_rating:
@@ -966,6 +974,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -973,6 +982,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -988,22 +998,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -1014,6 +1039,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -1032,9 +1058,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -1087,4 +1115,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_flexbox_flex.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_flexbox_flex.yml
index 4acd4eec77fc6cac2249e3f408ed13366f0a7f05..6cf3a6ac0ccf4b65ad2d85bf222bc32a5336ad62 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_flexbox_flex.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_flexbox_flex.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_flexbox_flex
 title: 'Test: Element: Flexbox flex '
 description: 'Test flexbox flex containers.'
@@ -811,6 +813,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -818,6 +821,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -833,22 +837,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -859,6 +878,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -877,9 +897,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -932,4 +954,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format.yml
index 53e2055781259366c81493bffd95b56b1abf35c3..721c55f5a50f50b350cf9eb578e69432312457cf 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_format
 title: 'Test: Element: Format'
 description: 'Test element formatting.'
@@ -556,30 +558,26 @@ elements: |
       webform_computed_token_value:
         '#type': webform_computed_token
         '#title': 'Computed token (Value)'
-        '#value': 'This is a Computed token value.'
-        '#default_value': Loremipsum
         '#format': value
+        '#template': 'This is a Computed token value.'
       webform_computed_token_raw:
         '#type': webform_computed_token
         '#title': 'Computed token (Raw value)'
-        '#value': 'This is a Computed token value.'
-        '#default_value': Loremipsum
         '#format': raw
+        '#template': 'This is a Computed token value.'
     webform_computed_twig:
       '#type': details
       '#title': 'Computed Twig'
       webform_computed_twig_value:
         '#type': webform_computed_twig
         '#title': 'Computed Twig (Value)'
-        '#value': 'This is a Computed Twig value.'
-        '#default_value': Loremipsum
         '#format': value
+        '#template': 'This is a Computed Twig value.'
       webform_computed_twig_raw:
         '#type': webform_computed_twig
         '#title': 'Computed Twig (Raw value)'
-        '#value': 'This is a Computed Twig value.'
-        '#default_value': Loremipsum
         '#format': raw
+        '#template': 'This is a Computed Twig value.'
   date_time_elements:
     '#type': details
     '#title': 'Date/time elements'
@@ -597,16 +595,21 @@ elements: |
         '#title': 'Date (Raw value)'
         '#default_value': '1942-06-18'
         '#format': raw
-      date_fallback:
-        '#type': date
-        '#title': 'Date (Fallback date format)'
-        '#default_value': '1942-06-18'
-        '#format': fallback
       date_html_date:
         '#type': date
         '#title': 'Date (HTML Date)'
         '#default_value': '1942-06-18'
         '#format': html_date
+      date_html_time:
+        '#type': date
+        '#title': 'Date (HTML Time)'
+        '#default_value': '1942-06-18'
+        '#format': html_time
+      date_fallback:
+        '#type': date
+        '#title': 'Date (Fallback date format)'
+        '#default_value': '1942-06-18'
+        '#format': fallback
       date_html_datetime:
         '#type': date
         '#title': 'Date (HTML Datetime)'
@@ -617,11 +620,6 @@ elements: |
         '#title': 'Date (HTML Month)'
         '#default_value': '1942-06-18'
         '#format': html_month
-      date_html_time:
-        '#type': date
-        '#title': 'Date (HTML Time)'
-        '#default_value': '1942-06-18'
-        '#format': html_time
       date_html_week:
         '#type': date
         '#title': 'Date (HTML Week)'
@@ -815,7 +813,7 @@ elements: |
         '#selection_handler': 'default:user'
         '#selection_settings':
           include_anonymous: true
-        '#default_value': 5
+        '#default_value': 1
         '#format': value
       entity_autocomplete_raw:
         '#type': entity_autocomplete
@@ -824,7 +822,7 @@ elements: |
         '#selection_handler': 'default:user'
         '#selection_settings':
           include_anonymous: true
-        '#default_value': 5
+        '#default_value': 1
         '#format': raw
       entity_autocomplete_link:
         '#type': entity_autocomplete
@@ -833,7 +831,7 @@ elements: |
         '#selection_handler': 'default:user'
         '#selection_settings':
           include_anonymous: true
-        '#default_value': 5
+        '#default_value': 1
         '#format': link
       entity_autocomplete_id:
         '#type': entity_autocomplete
@@ -842,7 +840,7 @@ elements: |
         '#selection_handler': 'default:user'
         '#selection_settings':
           include_anonymous: true
-        '#default_value': 5
+        '#default_value': 1
         '#format': id
       entity_autocomplete_label:
         '#type': entity_autocomplete
@@ -851,7 +849,7 @@ elements: |
         '#selection_handler': 'default:user'
         '#selection_settings':
           include_anonymous: true
-        '#default_value': 5
+        '#default_value': 1
         '#format': label
       entity_autocomplete_text:
         '#type': entity_autocomplete
@@ -860,7 +858,7 @@ elements: |
         '#selection_handler': 'default:user'
         '#selection_settings':
           include_anonymous: true
-        '#default_value': 5
+        '#default_value': 1
         '#format': text
       entity_autocomplete_teaser:
         '#type': entity_autocomplete
@@ -869,7 +867,7 @@ elements: |
         '#selection_handler': 'default:user'
         '#selection_settings':
           include_anonymous: true
-        '#default_value': 5
+        '#default_value': 1
         '#format': teaser
       entity_autocomplete_default:
         '#type': entity_autocomplete
@@ -878,8 +876,8 @@ elements: |
         '#selection_handler': 'default:user'
         '#selection_settings':
           include_anonymous: true
-        '#default_value': 5
-        '#format': default
+        '#default_value': 1
+        '#format': _default
     webform_entity_radios:
       '#type': details
       '#title': 'Entity radios'
@@ -986,7 +984,7 @@ elements: |
           1: Administrator
           0: Anonymous
         '#default_value': 1
-        '#format': default
+        '#format': _default
     webform_entity_select:
       '#type': details
       '#title': 'Entity select'
@@ -1085,7 +1083,7 @@ elements: |
           1: Administrator
           0: Anonymous
         '#default_value': 1
-        '#format': default
+        '#format': _default
     webform_term_select:
       '#type': details
       '#title': 'Term select'
@@ -1093,55 +1091,55 @@ elements: |
         '#type': webform_term_select
         '#title': 'Term select (Value)'
         '#vocabulary': tags
-        '#default_value': 3
+        '#default_value': Loremipsum
         '#format': value
       webform_term_select_raw:
         '#type': webform_term_select
         '#title': 'Term select (Raw value)'
         '#vocabulary': tags
-        '#default_value': 3
+        '#default_value': Loremipsum
         '#format': raw
       webform_term_select_link:
         '#type': webform_term_select
         '#title': 'Term select (Link)'
         '#vocabulary': tags
-        '#default_value': 3
+        '#default_value': Loremipsum
         '#format': link
       webform_term_select_id:
         '#type': webform_term_select
         '#title': 'Term select (Entity ID)'
         '#vocabulary': tags
-        '#default_value': 3
+        '#default_value': Loremipsum
         '#format': id
       webform_term_select_label:
         '#type': webform_term_select
         '#title': 'Term select (Label)'
         '#vocabulary': tags
-        '#default_value': 3
+        '#default_value': Loremipsum
         '#format': label
       webform_term_select_text:
         '#type': webform_term_select
         '#title': 'Term select (Label (ID))'
         '#vocabulary': tags
-        '#default_value': 3
+        '#default_value': Loremipsum
         '#format': text
       webform_term_select_teaser:
         '#type': webform_term_select
         '#title': 'Term select (Teaser)'
         '#vocabulary': tags
-        '#default_value': 3
+        '#default_value': Loremipsum
         '#format': teaser
       webform_term_select_default:
         '#type': webform_term_select
         '#title': 'Term select (Default)'
         '#vocabulary': tags
-        '#default_value': 3
-        '#format': default
+        '#default_value': Loremipsum
+        '#format': _default
       webform_term_select_breadcrumb:
         '#type': webform_term_select
         '#title': 'Term select (Breadcrumb)'
         '#vocabulary': tags
-        '#default_value': 3
+        '#default_value': Loremipsum
         '#format': breadcrumb
   file_upload_elements:
     '#type': details
@@ -1170,21 +1168,21 @@ elements: |
         '#title': 'File (Link)'
         '#file_extensions': txt
         '#format': link
-      managed_file_id:
+      managed_file_url:
         '#type': managed_file
-        '#title': 'File (File ID)'
+        '#title': 'File (URL)'
         '#file_extensions': txt
-        '#format': id
+        '#format': url
       managed_file_name:
         '#type': managed_file
         '#title': 'File (File name)'
         '#file_extensions': txt
         '#format': name
-      managed_file_url:
+      managed_file_id:
         '#type': managed_file
-        '#title': 'File (URL)'
+        '#title': 'File (File ID)'
         '#file_extensions': txt
-        '#format': url
+        '#format': id
   other_elements:
     '#type': details
     '#title': 'Other elements'
@@ -1210,6 +1208,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -1217,6 +1216,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -1232,22 +1232,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -1258,6 +1273,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -1276,9 +1292,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -1331,6 +1349,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_text:
     id: email
@@ -1342,23 +1364,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: false
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -1374,23 +1398,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format_custom.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format_custom.yml
index ea58e9b865a6eb0eb98f2eae54420c19ac0df631..e87a516e06eae9ca29b0c94902e1bf50680c09b8 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format_custom.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format_custom.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_format_custom
 title: 'Test: Element: Format custom'
 description: 'Test element custom formatting.'
@@ -19,9 +21,33 @@ elements: |
     '#format': custom
     '#format_html': |
       <em>{{ value }}</em>
+      
     '#format_text': |
       /{{ value }}/
+      
     '#default_value': '{textfield_custom}'
+  textfield_custom_token:
+    '#type': textfield
+    '#title': textfield_custom_token
+    '#format': custom
+    '#format_html': |
+      <em>[webform_submission:values:textfield_custom_token:raw]</em>
+      
+    '#format_text': |
+      /[webform_submission:values:textfield_custom_token:raw]/
+      
+    '#default_value': '{textfield_custom_token}'
+  textfield_custom_token_exception:
+    '#type': textfield
+    '#title': textfield_custom_token_exception
+    '#format': custom
+    '#format_html': |
+      <em>EXCEPTION[webform_submission:values:textfield_custom_token_exception]</em>
+      
+    '#format_text': |
+      /EXCEPTION[webform_submission:values:textfield_custom_token_exception]/
+      
+    '#default_value': '{textfield_custom_token_exception}'
   textfield_custom_value_multiple:
     '#type': textfield
     '#title': textfield_custom
@@ -29,8 +55,10 @@ elements: |
     '#format': custom
     '#format_html': |
       <em>{{ value }}</em>
+      
     '#format_text': |
       /{{ value }}/
+      
     '#format_items': custom
     '#format_items_html': |
       <table>
@@ -38,10 +66,12 @@ elements: |
         <tr {% if loop.index is divisible by(2) %}style="background-color: #ffc"{% endif %}><td>{{ item }}</td></tr>
       {% endfor %}
       </table>
+      
     '#format_items_text': |
       {% for item in items %}
       ⦿ {{ item }}
       {% endfor %}
+      
     '#default_value':
       - One
       - Two
@@ -62,6 +92,7 @@ elements: |
       item['original:image']: <div style="width: 100px">{{ item['original:image'] }}</div>
       item['original:link']: <div style="width: 100px">{{ item['original:link'] }}</div>
       item['original:modal']: <div style="width: 100px">{{ item['original:modal'] }}</div>
+      
     '#format_text': |
       value: {{ value }}
       item['value']: {{ item['value'] }}
@@ -69,6 +100,7 @@ elements: |
       item['link']: {{ item['link'] }}
       item['id']: {{ item['id'] }}
       item['url']: {{ item['url'] }}
+      
   address_custom:
     '#type': webform_address
     '#title': address_custom
@@ -80,6 +112,7 @@ elements: |
       element.state_province: {{ element.state_province }}<br/>
       element.postal_code: {{ element.postal_code }}<br/>
       element.country: {{ element.country }}<br/>
+      
     '#format_text': |
       element.address: {{ element.address }}
       element.address_2: {{ element.address_2 }}
@@ -87,6 +120,7 @@ elements: |
       element.state_province: {{ element.state_province }}
       element.postal_code: {{ element.postal_code }}
       element.country: {{ element.country }}
+      
     '#state_province__type': webform_select_other
     '#country__type': webform_select_other
     '#default_value':
@@ -96,14 +130,67 @@ elements: |
       state_province: '{state_province}'
       postal_code: '{postal_code}'
       country: '{country}'
+  address_multiple_custom:
+    '#type': webform_address
+    '#title': address_multiple_custom
+    '#multiple': true
+    '#default_value':
+      - address: '{01-address}'
+        address_2: '{01-address_2}'
+        city: '{01-city}'
+        state_province: '{01-state_province}'
+        postal_code: '{01-postal_code}'
+        country: '{01-country}'
+      - address: '{02-address}'
+        address_2: '{02-address_2}'
+        city: '{02-city}'
+        state_province: '{02-state_province}'
+        postal_code: '{02-postal_code}'
+        country: '{02-country}'
+    '#format': custom
+    '#format_html': |
+      element.address: {{ element.address }}<br/>
+      element.address_2: {{ element.address_2 }}<br/>
+      element.city: {{ element.city }}<br/>
+      element.state_province: {{ element.state_province }}<br/>
+      element.postal_code: {{ element.postal_code }}<br/>
+      element.country: {{ element.country }}<br/>
+      
+    '#format_text': |
+      element.address: {{ element.address }}
+      element.address_2: {{ element.address_2 }}
+      element.city: {{ element.city }}
+      element.state_province: {{ element.state_province }}
+      element.postal_code: {{ element.postal_code }}
+      element.country: {{ element.country }}
+      
+    '#format_items': custom
+    '#format_items_html': |
+      {% for item in items %}
+      <div>*****</div>
+      {{ item }}
+      <div>*****</div>
+      {% endfor %}
+      
+    '#format_items_text': |
+      {% for item in items %}
+      *****
+      {{ item }}
+      *****
+      {% endfor %}
+      
+    '#state_province__type': webform_select_other
+    '#country__type': webform_select_other
   fieldset_custom:
     '#type': fieldset
     '#title': fieldset_custom
     '#format': custom
     '#format_html': |
       {{ item.details }}
+      
     '#format_text': |
       {{ item.details }}
+      
     fieldset_custom_textfield:
       '#type': textfield
       '#title': fieldset_custom_textfield
@@ -116,10 +203,12 @@ elements: |
       <h3>fieldset_custom_children</h3>
       <hr />
       {{ children }}
+      
     '#format_text': |
       fieldset_custom_children
       ------------------------
       {{ children }}
+      
     fieldset_custom_children_textfield:
       '#type': textfield
       '#title': fieldset_custom_children_textfield
@@ -132,6 +221,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -139,6 +229,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -154,22 +245,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -180,6 +286,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -198,9 +305,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -253,4 +362,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format_multiple.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format_multiple.yml
index a7b2eed674244b3ecc2d547620ae50547b49495f..0a88507cf9d9ae51a9c7e0758dbf28760dfacbe5 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format_multiple.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format_multiple.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_format_multiple
 title: 'Test: Element: Format multiple values'
 description: 'Test element formatting with multiple value.'
@@ -1476,9 +1478,9 @@ elements: |
         '#vocabulary': tags
         '#multiple': true
         '#default_value':
-          - 41
-          - 48
-          - 43
+          - Loremipsum
+          - Oratione
+          - Dixisset
         '#format_items': comma
       webform_term_checkboxes_semicolon:
         '#type': webform_term_checkboxes
@@ -1486,9 +1488,9 @@ elements: |
         '#vocabulary': tags
         '#multiple': true
         '#default_value':
-          - 41
-          - 48
-          - 43
+          - Loremipsum
+          - Oratione
+          - Dixisset
         '#format_items': semicolon
       webform_term_checkboxes_and:
         '#type': webform_term_checkboxes
@@ -1496,9 +1498,9 @@ elements: |
         '#vocabulary': tags
         '#multiple': true
         '#default_value':
-          - 41
-          - 48
-          - 43
+          - Loremipsum
+          - Oratione
+          - Dixisset
         '#format_items': and
       webform_term_checkboxes_ol:
         '#type': webform_term_checkboxes
@@ -1506,9 +1508,9 @@ elements: |
         '#vocabulary': tags
         '#multiple': true
         '#default_value':
-          - 41
-          - 48
-          - 43
+          - Loremipsum
+          - Oratione
+          - Dixisset
         '#format_items': ol
       webform_term_checkboxes_ul:
         '#type': webform_term_checkboxes
@@ -1516,9 +1518,9 @@ elements: |
         '#vocabulary': tags
         '#multiple': true
         '#default_value':
-          - 41
-          - 48
-          - 43
+          - Loremipsum
+          - Oratione
+          - Dixisset
         '#format_items': ul
     webform_term_select:
       '#type': details
@@ -1529,9 +1531,9 @@ elements: |
         '#vocabulary': tags
         '#multiple': true
         '#default_value':
-          - 41
-          - 48
-          - 43
+          - Loremipsum
+          - Oratione
+          - Dixisset
         '#format_items': comma
       webform_term_select_semicolon:
         '#type': webform_term_select
@@ -1539,9 +1541,9 @@ elements: |
         '#vocabulary': tags
         '#multiple': true
         '#default_value':
-          - 41
-          - 48
-          - 43
+          - Loremipsum
+          - Oratione
+          - Dixisset
         '#format_items': semicolon
       webform_term_select_and:
         '#type': webform_term_select
@@ -1549,9 +1551,9 @@ elements: |
         '#vocabulary': tags
         '#multiple': true
         '#default_value':
-          - 41
-          - 48
-          - 43
+          - Loremipsum
+          - Oratione
+          - Dixisset
         '#format_items': and
       webform_term_select_ol:
         '#type': webform_term_select
@@ -1559,9 +1561,9 @@ elements: |
         '#vocabulary': tags
         '#multiple': true
         '#default_value':
-          - 41
-          - 48
-          - 43
+          - Loremipsum
+          - Oratione
+          - Dixisset
         '#format_items': ol
       webform_term_select_ul:
         '#type': webform_term_select
@@ -1569,9 +1571,46 @@ elements: |
         '#vocabulary': tags
         '#multiple': true
         '#default_value':
-          - 41
-          - 48
-          - 43
+          - Loremipsum
+          - Oratione
+          - Dixisset
+        '#format_items': ul
+  file_upload_elements:
+    '#type': details
+    '#title': 'File upload elements'
+    '#open': true
+    managed_file:
+      '#type': details
+      '#title': File
+      managed_file_comma:
+        '#type': managed_file
+        '#title': 'File (Comma)'
+        '#multiple': true
+        '#file_extensions': txt
+        '#format_items': comma
+      managed_file_semicolon:
+        '#type': managed_file
+        '#title': 'File (Semicolon)'
+        '#multiple': true
+        '#file_extensions': txt
+        '#format_items': semicolon
+      managed_file_and:
+        '#type': managed_file
+        '#title': 'File (And)'
+        '#multiple': true
+        '#file_extensions': txt
+        '#format_items': and
+      managed_file_ol:
+        '#type': managed_file
+        '#title': 'File (Ordered list)'
+        '#multiple': true
+        '#file_extensions': txt
+        '#format_items': ol
+      managed_file_ul:
+        '#type': managed_file
+        '#title': 'File (Unordered list)'
+        '#multiple': true
+        '#file_extensions': txt
         '#format_items': ul
 css: ''
 javascript: ''
@@ -1581,6 +1620,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -1588,6 +1628,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -1603,22 +1644,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -1629,6 +1685,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -1647,9 +1704,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -1702,6 +1761,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_text:
     id: email
@@ -1713,23 +1776,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: false
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -1745,23 +1810,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format_token.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format_token.yml
index c70c8fea615216fb690bde7630bbaaebb0c54c50..1cb58ade5ba8448f750078f16d56fc10bd4e839a 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format_token.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_format_token.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_format_token
 title: 'Test: Element: Format: Token'
 description: 'Test element formatting using tokens.'
@@ -32,6 +34,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -39,6 +42,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -54,22 +58,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -80,6 +99,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -98,9 +118,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -153,6 +175,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_html:
     id: email
@@ -164,16 +190,16 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
+      from_name: _default
+      subject: _default
       body: |
         <h3>default:</h3>[webform_submission:values:checkboxes]<hr />
         <h3>comma:</h3>[webform_submission:values:checkboxes:value:comma]<hr />
@@ -182,12 +208,15 @@ handlers:
         <h3>ul:</h3>[webform_submission:values:checkboxes:value:ul]<hr />
         <h3>ol:</h3>[webform_submission:values:checkboxes:value:ol]<hr />
         <h3>raw:</h3>[webform_submission:values:checkboxes:raw]<hr />
+        
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -203,16 +232,16 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
+      from_name: _default
+      subject: _default
       body: |
         default:
         [webform_submission:values:checkboxes]
@@ -234,12 +263,15 @@ handlers:
         
         raw:
         [webform_submission:values:checkboxes:raw]
+        
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: false
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_help.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_help.yml
index 16de96fe5c48f23a4a4cfc63a611653e487d7332..d5dad12f11b1275b2f625383e754b925ee6a9c65 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_help.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_help.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_help
 title: 'Test: Element: Help'
 description: 'Test element #help property.'
@@ -22,6 +24,11 @@ elements: |
     '#title': help_required
     '#required': true
     '#help': '{This is an example of help for a required element}'
+  help_title:
+    '#type': textfield
+    '#title': help_title
+    '#help': '{This is an example of help with a custom help title}'
+    '#help_title': '{Help custom title}'
   help_html:
     '#type': textfield
     '#title': help_html
@@ -71,6 +78,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -78,6 +86,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -93,22 +102,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -119,6 +143,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -137,9 +162,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -192,4 +219,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_horizontal_rule.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_horizontal_rule.yml
index b49794da34c37fa2f2318b14eff83a8f897f7931..6889d5d61ffeba25cef7cb23e9eaf38f724a2e00 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_horizontal_rule.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_horizontal_rule.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_horizontal_rule
 title: 'Test: Element: Horizontal rule'
 description: 'Test the horizontal rule element.'
@@ -107,6 +109,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -114,6 +117,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -129,22 +133,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -155,6 +174,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -173,9 +193,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -228,6 +250,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_html_editor.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_html_editor.yml
index a96234f183e1e58caf02c51f7def13527017b3e4..9f6dc72148396839c59e38ae616e7cf3f73c1580 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_html_editor.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_html_editor.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_html_editor
 title: 'Test: Element: HTML editor'
 description: 'Test HTML editor element.'
@@ -24,12 +26,15 @@ elements: |
     '#default_value': 'Hello <b>World!!!</b>'
     '#required': true
     '#disable': true
+    '#attributes':
+      class:
+        - custom-disabled
   webform_html_editor_format:
     '#type': webform_html_editor
     '#title': 'webform_html_editor (format)'
     '#required': true
     '#default_value': 'Hello <b>World!!!</b>'
-    '#format': 'basic_html'
+    '#format': basic_html
   webform_html_editor_codemirror:
     '#type': webform_html_editor
     '#title': 'webform_html_editor_codemirror (none)'
@@ -44,6 +49,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -51,6 +57,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -66,22 +73,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -92,6 +114,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -110,9 +133,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -165,6 +190,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_html_escape.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_html_escape.yml
index a7e5629f5672ac8de1d824215a518398bcdbf7b0..adee915d1beba023f2c6ab34ad69d815f5d9cda0 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_html_escape.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_html_escape.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_html_escape
 title: 'Test: Element: HTML escaping'
 description: 'Test element HTML escaping support'
@@ -98,7 +100,7 @@ elements: |
       '#min': 0
       '#max': 100
       '#step': 1
-      '#output': right
+      '#output': below
       '#output__field_prefix': $
       '#output__field_suffix': '.00'
     webform_rating:
@@ -158,9 +160,6 @@ elements: |
       '#message_type': warning
       '#message_message': 'This is a <strong>warning</strong> message.'
       '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/77245">Issue #77245: A place for JavaScript status messages</a>'
-    table:
-      '#type': table
-      '#title': 'Table | <script>alert(''This markup is not escaped properly!!!'') </script>'
   file_upload_elements:
     '#type': details
     '#title': 'File upload elements | <script>alert(''This markup is not escaped properly!!!'') </script>'
@@ -350,11 +349,11 @@ elements: |
     webform_computed_token:
       '#type': webform_computed_token
       '#title': 'Computed token | <script>alert(''This markup is not escaped properly!!!'') </script>'
-      '#value': 'This is a Computed token value.'
+      '#template': 'This is a Computed token value.'
     webform_computed_twig:
       '#type': webform_computed_twig
       '#title': 'Computed Twig | <script>alert(''This markup is not escaped properly!!!'') </script>'
-      '#value': 'This is a Computed Twig value.'
+      '#template': 'This is a Computed Twig value.'
   containers:
     '#type': details
     '#title': 'Containers | <script>alert(''This markup is not escaped properly!!!'') </script>'
@@ -376,9 +375,6 @@ elements: |
       '#field_prefix': '{field_prefix} | <script>alert(''This markup is not escaped properly!!!'') </script>'
       '#field_suffix': '{field_suffix} | <script>alert(''This markup is not escaped properly!!!'') </script>'
       '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/783438">Issue #783438: #states doesn''t work for #type item</a>'
-    label:
-      '#type': label
-      '#title': 'Label | <script>alert(''This markup is not escaped properly!!!'') </script>'
     webform_section:
       '#type': webform_section
       '#title': 'Section | <script>alert(''This markup is not escaped properly!!!'') </script>'
@@ -502,6 +498,13 @@ elements: |
       '#vocabulary': tags
       '#multiple': true
       '#select2': true
+  markup:
+    '#type': details
+    '#title': 'Markup | <script>alert(''This markup is not escaped properly!!!'') </script>'
+    '#open': true
+    label:
+      '#type': label
+      '#title': 'Label | <script>alert(''This markup is not escaped properly!!!'') </script>'
   other_elements:
     '#type': details
     '#title': 'Other elements | <script>alert(''This markup is not escaped properly!!!'') </script>'
@@ -512,6 +515,9 @@ elements: |
     machine_name:
       '#type': machine_name
       '#title': 'Machine name | <script>alert(''This markup is not escaped properly!!!'') </script>'
+    table:
+      '#type': table
+      '#title': 'Table | <script>alert(''This markup is not escaped properly!!!'') </script>'
 css: ''
 javascript: ''
 settings:
@@ -520,6 +526,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -527,6 +534,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -542,22 +550,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -568,6 +591,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -586,9 +610,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -641,4 +667,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_html_markup.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_html_markup.yml
index 71599403d9bc85e44d800c0c5585bcc111f40766..57d201ccf4871c2d4c1492fce5a23cf3ed20d327 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_html_markup.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_html_markup.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_html_markup
 title: 'Test: Element: HTML Markup'
 description: 'Test element HTML markup support by underlining all titles, descriptions, prefixes, and suffixes.'
@@ -98,7 +100,7 @@ elements: |
       '#min': 0
       '#max': 100
       '#step': 1
-      '#output': right
+      '#output': below
       '#output__field_prefix': $
       '#output__field_suffix': '.00'
     webform_rating:
@@ -158,9 +160,6 @@ elements: |
       '#message_type': warning
       '#message_message': 'This is a <strong>warning</strong> message.'
       '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/77245">Issue #77245: A place for JavaScript status messages</a>'
-    table:
-      '#type': table
-      '#title': '<u>Table</u>'
   file_upload_elements:
     '#type': details
     '#title': '<u>File upload elements</u>'
@@ -350,11 +349,11 @@ elements: |
     webform_computed_token:
       '#type': webform_computed_token
       '#title': '<u>Computed token</u>'
-      '#value': 'This is a Computed token value.'
+      '#template': 'This is a Computed token value.'
     webform_computed_twig:
       '#type': webform_computed_twig
       '#title': '<u>Computed Twig</u>'
-      '#value': 'This is a Computed Twig value.'
+      '#template': 'This is a Computed Twig value.'
   containers:
     '#type': details
     '#title': '<u>Containers</u>'
@@ -376,9 +375,6 @@ elements: |
       '#field_prefix': '<u>{field_prefix}</u>'
       '#field_suffix': '<u>{field_suffix}</u>'
       '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/783438">Issue #783438: #states doesn''t work for #type item</a>'
-    label:
-      '#type': label
-      '#title': '<u>Label</u>'
     webform_section:
       '#type': webform_section
       '#title': '<u>Section</u>'
@@ -502,6 +498,13 @@ elements: |
       '#vocabulary': tags
       '#multiple': true
       '#select2': true
+  markup:
+    '#type': details
+    '#title': '<u>Markup</u>'
+    '#open': true
+    label:
+      '#type': label
+      '#title': '<u>Label</u>'
   other_elements:
     '#type': details
     '#title': '<u>Other elements</u>'
@@ -512,6 +515,9 @@ elements: |
     machine_name:
       '#type': machine_name
       '#title': '<u>Machine name</u>'
+    table:
+      '#type': table
+      '#title': '<u>Table</u>'
 css: ''
 javascript: ''
 settings:
@@ -520,6 +526,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -527,6 +534,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -542,22 +550,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -568,6 +591,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -586,9 +610,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -641,4 +667,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_icheck.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_icheck.yml
index 101daad88a8035f9e7b501571ffcde8547f77cec..b91055d164174165f5a146f6cba8b8e0d2001faa 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_icheck.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_icheck.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_icheck
 title: 'Test: Element: iCheck'
 description: 'Test iCheck element.'
@@ -63,12 +65,12 @@ elements: |
         1: One
         2: Two
         3: Three
-        other: Other...
+        other: Other…
       '#icheck': flat
     radios_target:
       '#type': textfield
       '#attributes':
-        placeholder: 'Enter other...'
+        placeholder: 'Enter other…'
       '#states':
         visible:
           ':input[name="radios_trigger"]':
@@ -84,8 +86,8 @@ elements: |
       '#type': textfield
       '#title': 'Please enter a value'
     checkbox_target:
-      '#type': 'checkboxes'
-      '#title': 'Checkboxes'
+      '#type': checkboxes
+      '#title': Checkboxes
       '#options':
         1: One
         2: Two
@@ -103,6 +105,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -110,6 +113,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -125,22 +129,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -151,6 +170,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -169,9 +189,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -224,4 +246,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_icheck_styles.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_icheck_styles.yml
index 099bfb2210d184db4cac57322e52296a420068fd..d1888e665394611f9a3e27de5f4fce000e934423 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_icheck_styles.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_icheck_styles.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_icheck_styles
 title: 'Test: Element: iCheck style'
 description: 'Test iCheck style.'
@@ -27,7 +29,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': minimal
     minimal_radios:
       '#type': radios
@@ -49,7 +51,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': minimal-grey
     minimal_grey_radios:
       '#type': radios
@@ -71,7 +73,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': minimal-yellow
     minimal_yellow_radios:
       '#type': radios
@@ -93,7 +95,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': minimal-orange
     minimal_orange_radios:
       '#type': radios
@@ -115,7 +117,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': minimal-red
     minimal_red_radios:
       '#type': radios
@@ -137,7 +139,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': minimal-pink
     minimal_pink_radios:
       '#type': radios
@@ -159,7 +161,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': minimal-purple
     minimal_purple_radios:
       '#type': radios
@@ -181,7 +183,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': minimal-blue
     minimal_blue_radios:
       '#type': radios
@@ -203,7 +205,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': minimal-green
     minimal_green_radios:
       '#type': radios
@@ -225,7 +227,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': minimal-aero
     minimal_aero_radios:
       '#type': radios
@@ -251,7 +253,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': flat
     flat_radios:
       '#type': radios
@@ -273,7 +275,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': flat-grey
     flat_grey_radios:
       '#type': radios
@@ -295,7 +297,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': flat-yellow
     flat_yellow_radios:
       '#type': radios
@@ -317,7 +319,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': flat-orange
     flat_orange_radios:
       '#type': radios
@@ -339,7 +341,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': flat-red
     flat_red_radios:
       '#type': radios
@@ -361,7 +363,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': flat-pink
     flat_pink_radios:
       '#type': radios
@@ -383,7 +385,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': flat-purple
     flat_purple_radios:
       '#type': radios
@@ -405,7 +407,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': flat-blue
     flat_blue_radios:
       '#type': radios
@@ -427,7 +429,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': flat-green
     flat_green_radios:
       '#type': radios
@@ -449,7 +451,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': flat-aero
     flat_aero_radios:
       '#type': radios
@@ -475,7 +477,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': line
     line_radios:
       '#type': radios
@@ -497,7 +499,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': line-grey
     line_grey_radios:
       '#type': radios
@@ -519,7 +521,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': line-yellow
     line_yellow_radios:
       '#type': radios
@@ -541,7 +543,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': line-orange
     line_orange_radios:
       '#type': radios
@@ -563,7 +565,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': line-red
     line_red_radios:
       '#type': radios
@@ -585,7 +587,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': line-pink
     line_pink_radios:
       '#type': radios
@@ -607,7 +609,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': line-purple
     line_purple_radios:
       '#type': radios
@@ -629,7 +631,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': line-blue
     line_blue_radios:
       '#type': radios
@@ -651,7 +653,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': line-green
     line_green_radios:
       '#type': radios
@@ -673,7 +675,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': line-aero
     line_aero_radios:
       '#type': radios
@@ -699,7 +701,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': square
     square_radios:
       '#type': radios
@@ -721,7 +723,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': square-grey
     square_grey_radios:
       '#type': radios
@@ -743,7 +745,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': square-yellow
     square_yellow_radios:
       '#type': radios
@@ -765,7 +767,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': square-orange
     square_orange_radios:
       '#type': radios
@@ -787,7 +789,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': square-red
     square_red_radios:
       '#type': radios
@@ -809,7 +811,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': square-pink
     square_pink_radios:
       '#type': radios
@@ -831,7 +833,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': square-purple
     square_purple_radios:
       '#type': radios
@@ -853,7 +855,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': square-blue
     square_blue_radios:
       '#type': radios
@@ -875,7 +877,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': square-green
     square_green_radios:
       '#type': radios
@@ -897,7 +899,7 @@ elements: |
         three: Three
       '#default_value': one
       '#wrapper_attributes':
-        class: container-inline
+        - class: container-inline
       '#icheck': square-aero
     square_aero_radios:
       '#type': radios
@@ -917,6 +919,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -924,6 +927,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -939,22 +943,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -965,6 +984,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -983,9 +1003,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -1038,4 +1060,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_ignored_properties.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_ignored_properties.yml
index df34449eeff5c25cf90d87b524dc548caf2085ac..7395ae491385d4590c77da8c83f38b9b93c30ed8 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_ignored_properties.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_ignored_properties.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_ignored_properties
 title: 'Test: Element: Ignore properties'
 description: 'Test ignored element properties and callbacks.'
@@ -35,6 +37,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -42,6 +45,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -57,22 +61,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -83,6 +102,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -101,9 +121,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -156,4 +178,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_text.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_image_file.yml
similarity index 71%
rename from web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_text.yml
rename to web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_image_file.yml
index 60a8893d5094e65a0f48c3e1fc26cb981d909fef..0334117aa356809fc753ddacfe8891e42802e26c 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_text.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_image_file.yml
@@ -6,38 +6,22 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
-id: test_element_text
-title: 'Test: Element: Text'
-description: 'Tests text field and text area based elements.'
+archive: false
+id: test_element_image_file
+title: 'Test: Element: Image file upload'
+description: 'Tests image file upload element.'
 category: 'Test: Element'
 elements: |
-  '#attributes':
-    novalidate: novalidate
-  text_format_element:
-    '#type': details
-    '#title': 'Text format elements'
-    '#open': true
-    text_format:
-      '#type': text_format
-      '#title': text_format
-      '#hide_help': true
-  counter_elements:
-    '#type': details
-    '#title': 'Word and character counter'
-    '#open': true
-    counter_characters:
-      '#type': textfield
-      '#title': 'Character counter'
-      '#counter_type': character
-      '#counter_maximum': 10
-    counter_words:
-      '#type': textarea
-      '#title': 'Word counter'
-      '#counter_type': word
-      '#counter_maximum': 3
-      '#counter_message': 'word(s) left. This is a custom message'
+  webform_image_file:
+    '#type': webform_image_file
+    '#title': webform_image_file
+  webform_image_file_advanced:
+    '#type': webform_image_file
+    '#title': 'webform_image_file_advanced (max: 20x20)'
+    '#max_resolution': 20x20
 css: ''
 javascript: ''
 settings:
@@ -46,6 +30,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -53,6 +38,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -68,22 +54,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -94,6 +95,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -112,9 +114,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -167,6 +171,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_nested.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_image_resolution.yml
similarity index 64%
rename from web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_nested.yml
rename to web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_image_resolution.yml
index a54e9574094562a7bf3ba6ed6bf4433efd4a189a..7624f99b6937cd9f27a1179eb154c1fc010543e5 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_nested.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_image_resolution.yml
@@ -6,56 +6,25 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
-id: test_form_states_server_nested
-title: 'Test: Form API #states nested elements in a conditional hidden container'
-description: 'Test Drupal''s #states for nested elements in a conditional hidden container.'
-category: 'Test: Form API #states'
+archive: false
+id: test_element_image_resolution
+title: 'Test: Element: Image resolution'
+description: 'Tests image resolution element.'
+category: 'Test: Element'
 elements: |
-  visible_trigger:
-   '#type': checkbox
-   '#title': visible_trigger
-  visible_fieldset:
-   '#type': fieldset
-   '#title': visible_fieldset
-   '#states':
-     visible:
-       ':input[name="visible_trigger"]':
-         checked: true
-   visible_textfield:
-     '#type': textfield
-     '#title': visible_textfield
-     '#required': true
-   visible_custom_textfield:
-     '#type': textfield
-     '#title': visible_custom_textfield
-     '#states':
-       required:
-         ':input[name="visible_trigger"]':
-           checked: true
-         ':input[name="visible_textfield"]':
-           filled: true
-  visible_slide_fieldset:
-   '#type': fieldset
-   '#title': visible_slide_fieldset
-   '#states':
-     'visible-slide':
-       ':input[name="visible_trigger"]':
-         checked: true
-   visible_slide_textfield:
-     '#type': textfield
-     '#title': visible_slide_textfield
-     '#required': true
-   visible_slide_custom_textfield:
-     '#type': textfield
-     '#title': visible_slide_custom_textfield
-     '#states':
-       required:
-         ':input[name="visible_trigger"]':
-           checked: true
-         ':input[name="visible_slide_textfield"]':
-           filled: true           
+  webform_image_resolution:
+    '#type': webform_image_resolution
+    '#title': webform_image_resolution
+  webform_image_resolution_advanced:
+    '#type': webform_image_resolution
+    '#title': webform_image_resolution_advanced
+    '#description': '{description}'
+    '#height_title': '{height_title}'
+    '#width_title': '{width_title}'
+    '#default_value': 300x400
 css: ''
 javascript: ''
 settings:
@@ -64,6 +33,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -71,6 +41,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -78,7 +49,7 @@ settings:
   form_prepopulate_source_entity_type: ''
   form_reset: false
   form_disable_autocomplete: false
-  form_novalidate: true
+  form_novalidate: false
   form_disable_inline_errors: false
   form_required: false
   form_unsaved: false
@@ -86,22 +57,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -112,6 +98,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -130,9 +117,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -140,7 +129,7 @@ settings:
   purge: none
   purge_days: null
   results_disabled: true
-  results_disabled_ignore: true
+  results_disabled_ignore: false
   token_update: false
 access:
   create:
@@ -185,4 +174,16 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
-handlers: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 1
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_input_mask.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_input_mask.yml
new file mode 100644
index 0000000000000000000000000000000000000000..34afc537fc02c3e74ac2641fceface6ecefcb936
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_input_mask.yml
@@ -0,0 +1,239 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_input_mask
+title: 'Test: Element:  Input Mask'
+description: 'Tests input mask.'
+category: 'Test: Element'
+elements: |
+  currency:
+    '#type': textfield
+    '#title': currency
+    '#input_mask': '''alias'': ''currency'''
+  datetime:
+    '#type': textfield
+    '#title': datetime
+    '#input_mask': '''alias'': ''datetime'''
+  decimal:
+    '#type': textfield
+    '#title': decimal
+    '#input_mask': '''alias'': ''decimal'''
+  email:
+    '#type': textfield
+    '#title': email
+    '#input_mask': '''alias'': ''email'''
+  ip:
+    '#type': textfield
+    '#title': ip
+    '#input_mask': '''alias'': ''ip'''
+  license_plate:
+    '#type': textfield
+    '#title': license_plate
+    '#input_mask': '[9-]AAA-999'
+  mac:
+    '#type': textfield
+    '#title': mac
+    '#input_mask': '''alias'': ''mac'''
+  percentage:
+    '#type': textfield
+    '#title': percentage
+    '#input_mask': '''alias'': ''percentage'''
+  phone:
+    '#type': textfield
+    '#title': phone
+    '#input_mask': '(999) 999-9999'
+  ssn:
+    '#type': textfield
+    '#title': ssn
+    '#input_mask': 999-99-9999
+  vin:
+    '#type': textfield
+    '#title': vin
+    '#input_mask': '''alias'': ''vin'''
+  zip:
+    '#type': textfield
+    '#title': zip
+    '#input_mask': '99999[-9999]'
+  uppercase:
+    '#type': textfield
+    '#title': uppercase
+    '#input_mask': '''casing'': ''upper'''
+  lowercase:
+    '#type': textfield
+    '#title': lowercase
+    '#input_mask': '''casing'': ''lower'''
+  custom:
+    '#type': textfield
+    '#title': custom
+    '#input_mask': '''alias'': ''numeric'', ''groupSeparator'': '','', ''autoGroup'': true, ''digits'': 2, ''digitsOptional'': false, ''prefix'': ''$ '', ''placeholder'': ''0'''
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 1
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_invalid.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_invalid.yml
index 90e9e2c7270c68116eb4c32e67e14448f96283a1..b3f8f3f1c6d1d97e4aa503c15a977f8c4a77e22c 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_invalid.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_invalid.yml
@@ -6,16 +6,15 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_invalid
 title: 'Test: Element: Invalid'
 description: 'Test invalid elements YAML.'
 category: 'Test: Element'
-elements: |
-  not
-  valid
-  yaml
+elements: '''not valid yaml'''
 css: ''
 javascript: ''
 settings:
@@ -24,6 +23,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -31,6 +31,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -46,22 +47,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -72,6 +88,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -90,9 +107,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -145,4 +164,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_likert.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_likert.yml
index 049ebd4657a393e053faec03413a17a038131471..95358fe1f549cb91d1f811069c25bca6aa53afa0 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_likert.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_likert.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_likert
 title: 'Test: Element: Likert'
 description: 'Test the likert element.'
@@ -55,28 +57,27 @@ elements: |
     '#title': likert_help
     '#questions_description_display': help
     '#questions':
-      q1: 'Question 1 -- This is a help text'
-      q2: 'Question 2 -- This is a help text'
-      q3: 'Question 3 -- This is a help text'
+      q1: 'Question 1 -- This is help text'
+      q2: 'Question 2 -- This is help text'
+      q3: 'Question 3 -- This is help text'
     '#answers_description_display': help
     '#answers':
-      1: 'Option 1 -- This is a help text'
-      2: 'Option 2 -- This is a help text'
-      3: 'Option 3 -- This is a help text'
+      1: 'Option 1 -- This is help text'
+      2: 'Option 2 -- This is help text'
+      3: 'Option 3 -- This is help text'
   likert_values:
     '#type': webform_likert
     '#title': likert_values
     '#na_answer': true
     '#questions':
-      0: 'Question 0'
-      1: 'Question 1'
-      2: 'Question 2'
+      - 'Question 0'
+      - 'Question 1'
+      - 'Question 2'
     '#answers':
-      0: 'Option 0'
-      1: 'Option 1'
-      2: 'Option 2'
-      3: 'Option 3'
-
+      - 'Option 0'
+      - 'Option 1'
+      - 'Option 2'
+      - 'Option 3'
 css: ''
 javascript: ''
 settings:
@@ -85,6 +86,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -92,6 +94,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -107,22 +110,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -133,6 +151,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -151,9 +170,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -206,6 +227,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_location.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_loc_geocomplete.yml
similarity index 79%
rename from web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_location.yml
rename to web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_loc_geocomplete.yml
index 544f0c70dbe502f23e7628b40c42644ce9a58588..48fd4be963320ae54ff7e2e5944bc671caaa3d61 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_location.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_loc_geocomplete.yml
@@ -6,11 +6,13 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
-id: test_element_location
-title: 'Test: Element: Location'
-description: 'Test (geo)location elements.'
+archive: false
+id: test_element_loc_geocomplete
+title: 'Test: Element: Location Geocomplete'
+description: 'Test (geo)location elements with Geocomplete.'
 category: 'Test: Element'
 elements: |
   location_default_element:
@@ -18,10 +20,10 @@ elements: |
     '#title': 'Location default element'
     '#open': true
     location_default:
-      '#type': webform_location
+      '#type': webform_location_geocomplete
       '#title': 'location default'
     location_map:
-      '#type': webform_location
+      '#type': webform_location_geocomplete
       '#title': 'location map'
       '#geolocation': true
       '#map': true
@@ -30,7 +32,7 @@ elements: |
     '#title': 'Location with visible attributes element'
     '#open': true
     location_attributes:
-      '#type': webform_location
+      '#type': webform_location_geocomplete
       '#title': 'Location with visible attributes'
       '#lat__access': true
       '#lng__access': true
@@ -50,7 +52,7 @@ elements: |
     '#title': 'Location with default value and required element'
     '#open': true
     location_default_value_required:
-      '#type': webform_location
+      '#type': webform_location_geocomplete
       '#title': 'Location with default value and required'
       '#default_value':
         value: '1600 Pennsylvania Ave NW, Washington, DC 20500'
@@ -73,7 +75,7 @@ elements: |
     '#title': 'Location with geolocation element'
     '#open': true
     location_geolocation:
-      '#type': webform_location
+      '#type': webform_location_geocomplete
       '#title': 'Location with geolocation'
       '#geolocation': true
       '#location__access': true
@@ -88,7 +90,7 @@ elements: |
       '#country__access': true
       '#country_short__access': true
   location_geolocation_hidden:
-    '#type': webform_location
+    '#type': webform_location_geocomplete
     '#title': 'Location with geolocation and hidden'
     '#geolocation': true
     '#hidden': true
@@ -100,6 +102,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -107,6 +110,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -122,22 +126,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -148,6 +167,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -166,9 +186,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -221,4 +243,16 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
-handlers: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 0
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_loc_places.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_loc_places.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3ef606eef2082d3abc51fbdf5c6cc83a394f3ee0
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_loc_places.yml
@@ -0,0 +1,212 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_loc_places
+title: 'Test: Element: Location Places'
+description: 'Test (geo)location elements with Algolia Places.'
+category: 'Test: Element'
+elements: |
+  location_default:
+    '#type': webform_location_places
+    '#title': location_default
+    '#placeholder': 'Enter a location'
+  location_attributes:
+    '#type': webform_location_places
+    '#title': location_attributes
+    '#lat__access': true
+    '#lng__access': true
+    '#name__access': true
+    '#postcode__access': true
+    '#locality__access': true
+    '#city__access': true
+    '#administrative__access': true
+    '#country__access': true
+    '#country_code__access': true
+    '#county__access': true
+    '#suburb__access': true
+  location_attributes_required:
+    '#type': webform_location_places
+    '#title': location_attributes_required
+    '#required': true
+    '#lat__access': true
+    '#lng__access': true
+    '#name__access': true
+    '#postcode__access': true
+    '#locality__access': true
+    '#city__access': true
+    '#administrative__access': true
+    '#country__access': true
+    '#country_code__access': true
+    '#county__access': true
+    '#suburb__access': true
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 0
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file.yml
index 61e556c2120f050aa553f419966a76201379613a..df7253c15a5c2f554a5865fb1b847751a7e5b730 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_managed_file
 title: 'Test: Element: Managed file'
 description: 'Test file uploads using managed file element.'
@@ -46,6 +48,17 @@ elements: |
     '#title': managed_file_sanitize
     '#file_extensions': txt
     '#sanitize': true
+  managed_file_single_placeholder:
+    '#type': managed_file
+    '#title': managed_file_single_placeholder
+    '#file_extensions': txt
+    '#file_placeholder': 'This is the single file upload placeholder'
+  managed_file_multiple_placeholder:
+    '#type': managed_file
+    '#title': managed_file_multiple_placeholder
+    '#file_extensions': txt
+    '#file_placeholder': 'This is the multiple file upload placeholder'
+    '#multiple': true
 css: ''
 javascript: ''
 settings:
@@ -54,6 +67,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -61,6 +75,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -76,22 +91,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -102,6 +132,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -120,9 +151,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -175,4 +208,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_dis.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_dis.yml
index 6d0a87cd048a4bbd24b5444b12f672129eefb66e..8ee36bdbffbe61014bfdea79e3074fde7d78f25d 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_dis.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_dis.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_managed_file_dis
 title: 'Test: Element: Managed file results disabled'
 description: 'Test file uploads using managed file element with results disabled.'
@@ -25,6 +27,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -32,6 +35,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -47,22 +51,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -73,6 +92,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -91,9 +111,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -146,4 +168,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_help.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_help.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2070b139c2b2d57d25ae15c8114bd90242394a7f
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_help.yml
@@ -0,0 +1,228 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_managed_file_help
+title: 'Test: File upload help'
+description: 'Test file upload help.'
+category: 'Test: Element'
+elements: |
+  description:
+    '#type': details
+    '#title': description
+    '#open': true
+    managed_file_help_description:
+      '#type': managed_file
+      '#title': managed_file_help_description
+    managed_file_help_description_custom:
+      '#type': managed_file
+      '#title': managed_file_help_description_custom
+      '#description': 'This is a custom description'
+      '#description_display': before
+      '#multiple': true
+  help:
+    '#type': details
+    '#title': help
+    '#open': true
+    managed_file_help_help:
+      '#type': managed_file
+      '#title': managed_file_help_help
+      '#file_help': help
+    managed_file_help_help_custom:
+      '#type': managed_file
+      '#title': managed_file_help_help_custom
+      '#file_help': help
+      '#help': 'This is custom help'
+      '#multiple': true
+  more:
+    '#type': details
+    '#title': more
+    '#open': true
+    managed_file_help_more:
+      '#type': managed_file
+      '#title': managed_file_help_more
+      '#file_help': more
+    managed_file_help_more_custom:
+      '#type': managed_file
+      '#title': managed_file_help_more_custom
+      '#file_help': more
+      '#more': 'This is custom help'
+      '#multiple': true
+  none:
+    '#type': details
+    '#title': none
+    '#open': true
+    managed_file_help_none:
+      '#type': managed_file
+      '#title': managed_file_help_none
+      '#file_help': none
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 0
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_limit.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_limit.yml
new file mode 100644
index 0000000000000000000000000000000000000000..416848c0c95ab45e2e8e029d3c8d43e0b8ad2fd5
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_limit.yml
@@ -0,0 +1,183 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_managed_file_limit
+title: 'Test: Element: Managed file limit'
+description: 'Test file upload limit per webform.'
+category: 'Test: Element'
+elements: |
+  managed_file_01:
+    '#type': managed_file
+    '#title': managed_file_01
+    '#file_extensions': txt
+  managed_file_02:
+    '#type': managed_file
+    '#title': managed_file_02
+    '#file_extensions': txt
+  managed_file_03:
+    '#type': managed_file
+    '#title': managed_file_03
+    '#file_extensions': txt
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: '1 MB'
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_name.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_name.yml
index 454137a9f633b0d6f045918a490b172394827aaa..757265c7cdc5995967c1137fa4cea0e36a86e449 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_name.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_name.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_managed_file_name
 title: 'Test: File rename'
 description: 'Dummy form to test uploaded file renaming.'
@@ -33,6 +35,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -40,6 +43,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -55,22 +59,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -81,6 +100,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -99,9 +119,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -154,6 +176,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_prev.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_prev.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7e28be31f4edd8990c114aaa0a20fe9fbaad069b
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_managed_file_prev.yml
@@ -0,0 +1,217 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_managed_file_prev
+title: 'Test: Element: Managed file preview'
+description: 'Test file upload previews using managed file element.'
+category: 'Test: Element'
+elements: |
+  webform_image_file:
+    '#type': webform_image_file
+    '#title': webform_image_file
+    '#file_preview': 'thumbnail:modal'
+    '#file_extensions': gif
+  webform_image_file_multiple:
+    '#type': webform_image_file
+    '#title': webform_image_file_multiple
+    '#file_preview': 'thumbnail:modal'
+    '#file_extensions': gif
+    '#multiple': true
+  webform_audio_file:
+    '#type': webform_audio_file
+    '#title': webform_audio_file
+    '#file_preview': file
+    '#file_extensions': mp3
+  webform_audio_file_multiple:
+    '#type': webform_audio_file
+    '#title': webform_audio_file_multiple
+    '#file_preview': file
+    '#file_extensions': mp3
+    '#multiple': true
+  webform_video_file:
+    '#type': webform_video_file
+    '#title': webform_video_file
+    '#file_preview': file
+    '#file_extensions': mp4
+  webform_video_file_multiple:
+    '#type': webform_video_file
+    '#title': webform_video_file_multiple
+    '#file_preview': file
+    '#file_extensions': mp4
+    '#multiple': true
+  webform_document_file:
+    '#type': webform_document_file
+    '#title': webform_document_file
+    '#file_preview': url
+    '#file_extensions': txt
+  webform_document_file_multiple:
+    '#type': webform_document_file
+    '#title': webform_document_file_multiple
+    '#file_preview': url
+    '#file_extensions': txt
+    '#multiple': true
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_mapping.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_mapping.yml
index e0fc82c0838c06ff19c54c760a0ed4a8e015990c..612c2acce2cf38d3d8a35869b588d9644d3d8222 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_mapping.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_mapping.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_mapping
 title: 'Test: Element: Mapping'
 description: 'Test the mapping element.'
@@ -34,7 +36,7 @@ elements: |
       '#source__title': '{Custom source}'
       '#destination__title': '{Destination source}'
       '#destination__description': '{Destination description}'
-      '#arrow': '»'
+      '#arrow': »
       '#format': table
       '#source': days
       '#destination':
@@ -113,6 +115,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -120,6 +123,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -135,22 +139,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -161,6 +180,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -179,9 +199,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -234,6 +256,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_markup.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_markup.yml
index cdcac3dde477ac1a35f2d9b707c8e1f361698278..4d1652e94c3d12cf97defcfd82b6eac6d8665b4f 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_markup.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_markup.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_markup
 title: 'Test: Element: Markup'
 description: 'Test the markup element.'
@@ -27,6 +29,10 @@ elements: |
     '#type': webform_markup
     '#markup': '<p>This is displayed on the both the form and submission view.</p>'
     '#display_on': both
+  markup_altered:
+    '#type': webform_markup
+    '#markup': '<p>Alter this markup.</p>'
+    '#display_on': both
 css: ''
 javascript: ''
 settings:
@@ -35,6 +41,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -42,6 +49,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -57,22 +65,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -83,6 +106,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -101,9 +125,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -156,4 +182,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_media_file.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_media_file.yml
index 36a391dd65d86024af2a180054cde53c0e066724..3b512287a97cb60854488820f463ad4764c7d72f 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_media_file.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_media_file.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_media_file
 title: 'Test: Element: Media type file upload'
 description: 'Test file uploads using media accepts and capture file element.'
@@ -77,6 +79,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -84,6 +87,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -99,22 +103,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -125,6 +144,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -143,9 +163,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -198,4 +220,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_message.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_message.yml
index 7a24f4bf583b4cee46c0fcbb64b14c9e6cfb47cc..2a6244ee134ffa24d6686dd8d71b37a09c21ab33 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_message.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_message.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_message
 title: 'Test: Element: Message'
 description: 'Test the message element.'
@@ -97,6 +99,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -104,6 +107,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -119,22 +123,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -145,6 +164,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -163,9 +183,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -218,4 +240,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_more.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_more.yml
index 8324d7df2fa8e702e280abadccaea6d121741cc6..be64567e15df18673a9c01227605f09a28b7354d 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_more.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_more.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_more
 title: 'Test: Element: More'
 description: 'Test element #more and #more_title properties.'
@@ -36,7 +38,7 @@ elements: |
     '#type': textfield
     '#title': more_title_description_hidden
     '#description': '{This is an example of a hidden description}'
-    '#description_display': 'invisible'
+    '#description_display': invisible
     '#more_title': '{Custom more title}'
     '#more': '{This is an example of more}'
   more_datetime:
@@ -67,6 +69,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -74,6 +77,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -89,22 +93,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -115,6 +134,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -133,9 +153,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -188,4 +210,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple.yml
index 893f38397a837b20998debdf840f324b688fcf10..af36c473f28d4d0bc1d11a414630d3d07d9d08fd 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_multiple
 title: 'Test: Element: Multiple'
 description: 'Test multiple element.'
@@ -36,6 +38,23 @@ elements: |
       - One
       - Two
       - Three
+  webform_multiple_no_add_more:
+    '#type': webform_multiple
+    '#title': webform_multiple_no_add_more
+    '#add_more': false
+    '#default_value':
+      - One
+      - Two
+      - Three
+  webform_multiple_custom_label:
+    '#type': webform_multiple
+    '#title': webform_multiple_custom_label
+    '#add_more_button_label': '{add_more_button_label}'
+    '#add_more_input_label': '{add_more_input_label}'
+    '#default_value':
+      - One
+      - Two
+      - Three
   webform_multiple_email_five:
     '#type': webform_multiple
     '#title': webform_multiple_email_five
@@ -189,7 +208,7 @@ elements: |
         '#help': 'This is custom help text'
       option:
         '#type': container
-        '#title': 'text/description'
+        '#title': text/description
         '#title_display': invisible
         '#help': 'This is custom help text'
         text:
@@ -205,10 +224,15 @@ elements: |
     '#default_value':
       - value: one
         text: One
-        description: This is the number 1.
+        description: 'This is the number 1.'
       - value: two
         text: Two
-        description: This is the number 2.
+        description: 'This is the number 2.'
+  webform_multiple_no_items:
+    '#type': webform_multiple
+    '#title': webform_multiple_no_items
+    '#min_items': 0
+    '#empty_items': 0
 css: ''
 javascript: ''
 settings:
@@ -217,6 +241,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -224,6 +249,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -239,22 +265,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -265,6 +306,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -283,9 +325,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -338,6 +382,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple_date.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple_date.yml
index c756272be63b5f3c13de15e880cca73abb8951fb..d7f5a9103cbd9f594a1f22dd06b6cadecafaa91a 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple_date.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple_date.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_multiple_date
 title: 'Test: Element: Multiple: Date'
 description: 'Test multiple text Date elements.'
@@ -27,15 +29,15 @@ elements: |
     '#open': true
     date_basic:
       '#type': date
-      '#title': 'date_basic'
+      '#title': date_basic
       '#multiple': true
     datetime_basic:
       '#type': datetime
-      '#title': 'datetime_basic'
+      '#title': datetime_basic
       '#multiple': true
     datelist_basic:
       '#type': datelist
-      '#title': 'datelist_basic'
+      '#title': datelist_basic
       '#multiple': true
 css: ''
 javascript: ''
@@ -45,6 +47,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -52,6 +55,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -67,22 +71,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -93,6 +112,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -111,9 +131,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -166,6 +188,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple_property.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple_property.yml
index 17ba0d916a9e05482f723b732582b2f29519ce4f..36a1b99891260dc95fd0e9640f7ef51ac2e62aba 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple_property.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple_property.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_multiple_property
 title: 'Test: Element: Multiple property'
 description: 'Test #multiple element property.'
@@ -19,11 +21,11 @@ elements: |
   webform_element_multiple_true:
     '#type': webform_element_multiple
     '#title': webform_element_multiple_true
-    '#default_value': TRUE
+    '#default_value': true
   webform_element_multiple_false:
     '#type': webform_element_multiple
     '#title': webform_element_multiple_false
-    '#default_value': FALSE
+    '#default_value': false
   webform_element_multiple_custom:
     '#type': webform_element_multiple
     '#title': webform_element_multiple_custom
@@ -41,6 +43,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -48,6 +51,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -63,22 +67,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -89,6 +108,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -107,9 +127,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -162,6 +184,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple_text.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple_text.yml
index 6052cf556451ca120fc1ee4d3f23689afb956066..17a71aabf37635d4cd1f15edafd507f3eed84426 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple_text.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_multiple_text.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_multiple_text
 title: 'Test: Element: Multiple: Text Based'
 description: 'Test multiple text based elements.'
@@ -88,6 +90,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -95,6 +98,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -110,22 +114,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -136,6 +155,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -154,9 +174,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -209,6 +231,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_options.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_options.yml
index 7069ade79b0fd831b17e311baa686f75740d54d8..471c67c7c46ac64fbc73081ee4bbce58e5685d88 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_options.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_options.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_options
 title: 'Test: Element: Options'
 description: 'Test options element.'
@@ -22,14 +24,14 @@ elements: |
       '#title': webform_options
     webform_options_default_value:
       '#type': webform_options
-      '#title': 'webform_options_default_value'
+      '#title': webform_options_default_value
       '#default_value':
         one: One
         two: Two
         three: Three
     webform_options_maxlength:
       '#type': webform_options
-      '#title': 'webform_options_maxlength'
+      '#title': webform_options_maxlength
       '#options_text_maxlength': 20
       '#options_value_maxlength': 20
       '#default_value':
@@ -38,7 +40,7 @@ elements: |
         three: Three
     webform_options_optgroup:
       '#type': webform_options
-      '#title': 'webform_options_optgroup'
+      '#title': webform_options_optgroup
       '#required': true
       '#default_value':
         'Group One':
@@ -53,12 +55,12 @@ elements: |
     '#open': true
     webform_element_options_entity:
       '#type': webform_element_options
-      '#title': 'webform_element_options_entity'
+      '#title': webform_element_options_entity
       '#required': true
       '#default_value': yes_no
     webform_element_options_custom:
       '#type': webform_element_options
-      '#title': 'webform_element_options_custom'
+      '#title': webform_element_options_custom
       '#required': true
       '#default_value':
         one: One
@@ -72,6 +74,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -79,6 +82,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -94,22 +98,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -120,6 +139,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -138,9 +158,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -193,6 +215,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_other.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_other.yml
index 45186d922d2250ac0d1b959c69ad73c8e74dfda3..3033c02fc7506bf7c13521b161294064de2e0cdd 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_other.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_other.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_other
 title: 'Test: Element: Other'
 description: 'Test the webform other elements, includes select_other, checkboxes_other, radios_other, and buttons_other.'
@@ -35,13 +37,16 @@ elements: |
         'Option Group':
           Two: Two
         Three: Three
-      '#empty_option': Select...
+      '#empty_option': Select…
       '#other__option_label': 'Is there another option you wish to enter?'
       '#other__title': Other
       '#other__placeholder': 'What is this other option'
       '#other__description': 'Other select description'
       '#other__size': 20
       '#other__maxlength': 20
+      '#other__counter_type': character
+      '#other__counter_minimum': '4'
+      '#other__counter_maximum': '10'
       '#required': true
     select_other_multiple:
       '#type': webform_select_other
@@ -75,15 +80,16 @@ elements: |
       '#type': webform_checkboxes_other
       '#title': 'Checkboxes other advanced'
       '#options':
-        One: One
-        Two: Two
-        Three: Three
+        One: 'One -- Description'
+        Two: 'Two -- Description'
+        Three: 'Three -- Description'
       '#default_value':
         - One
         - Two
         - Four
       '#required': true
       '#options_display': two_columns
+      '#options_description_display': help
       '#other__option_label': 'Is there another option you wish to enter?'
       '#other__placeholder': 'What is this other option'
       '#other__description': 'Other checkbox description'
@@ -103,12 +109,13 @@ elements: |
       '#type': webform_radios_other
       '#title': 'Radios other advanced'
       '#options':
-        One: One
-        Two: Two
-        Three: Three
+        One: 'One -- Description'
+        Two: 'Two -- Description'
+        Three: 'Three -- Description'
       '#default_value': Four
       '#required': true
       '#options_display': two_columns
+      '#options_description_display': help
       '#other__option_label': 'Is there another option you wish to enter?'
       '#other__placeholder': 'What is this other option'
       '#other__description': 'Other radio description'
@@ -136,6 +143,34 @@ elements: |
       '#other__option_label': Other
       '#other__placeholder': 'What is this other option'
       '#other__description': 'Other button description'
+  wrapper_other:
+    '#type': details
+    '#title': 'Wrapper other (#wrapper_type)'
+    '#open': true
+    wrapper_other_fieldset:
+      '#type': webform_select_other
+      '#title': 'Wrapper other (fieldset)'
+      '#options':
+        One: One
+        Two: Two
+        Three: Three
+      '#wrapper_type': fieldset
+    wrapper_other_form_element:
+      '#type': webform_select_other
+      '#title': 'Wrapper other (form_element)'
+      '#options':
+        One: One
+        Two: Two
+        Three: Three
+      '#wrapper_type': form_element
+    wrapper_other_container:
+      '#type': webform_select_other
+      '#title': 'Wrapper other (container)'
+      '#options':
+        One: One
+        Two: Two
+        Three: Three
+      '#wrapper_type': container
 css: ''
 javascript: ''
 settings:
@@ -144,6 +179,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -151,6 +187,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -166,22 +203,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -192,6 +244,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -210,9 +263,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -265,6 +320,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_pattern.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_pattern.yml
new file mode 100644
index 0000000000000000000000000000000000000000..87ebfc81d5d5383eb4e1d6a853e64316ca47cfb5
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_pattern.yml
@@ -0,0 +1,195 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_pattern
+title: 'Test: Element: Pattern'
+description: 'Test element pattern validation.'
+category: 'Test: Element'
+elements: |
+  pattern:
+    '#type': textfield
+    '#title': pattern
+    '#pattern': Hello
+    '#description': 'Enter ''Hello'''
+  pattern_error:
+    '#type': textfield
+    '#title': pattern_error
+    '#pattern': Hello
+    '#pattern_error': 'You did not enter ''Hello'''
+    '#description': 'Enter ''Hello'''
+  pattern_unicode:
+    '#type': textfield
+    '#title': pattern_unicode
+    '#pattern': \u2E8F
+    '#description': 'Enter unicode CJK characters ''⺏'''
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: true
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 0
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_prepopulate.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_prepopulate.yml
index eb2b331d643af803a4dea86083427ef309a870b1..88d35ed25c16901990d0661b4d73cb273629d5f2 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_prepopulate.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_prepopulate.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_prepopulate
 title: 'Test: Element: Prepopulate'
 description: 'Test prepopulating individual elements using query string parameters.'
@@ -19,11 +21,11 @@ elements: |
   textfield_prepopulate:
     '#type': textfield
     '#title': textfield_prepopulate
-    '#prepopulate':  true
+    '#prepopulate': true
   managed_file_prepopulate:
     '#type': managed_file
     '#title': managed_file_prepopulate
-    '#prepopulate':  true
+    '#prepopulate': true
   prepopulate_test:
     '#markup': '<a href="?textfield=value&textfield_prepopulate=value">?textfield=value&textfield_prepopulate=value</a>'
 css: ''
@@ -34,6 +36,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -41,6 +44,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -56,22 +60,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -82,6 +101,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -100,9 +120,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -155,4 +177,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_private.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_private.yml
index 06f6194953ccdd80e7ab1f2836f20021b5f25fce..8860f52ca1c8f08b124e5988d6fd6f9f988ebc83 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_private.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_private.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_private
 title: 'Test: Element: Private'
 description: 'Test element private properties'
@@ -29,6 +31,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -36,6 +39,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -51,22 +55,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -77,6 +96,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -95,9 +115,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -150,4 +172,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_radios.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_radios.yml
index 0d25675469e6105f5815389fa007741a171ecb89..d8437d7f2d105d1cef38fff567ea00a851207547 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_radios.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_radios.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_radios
 title: 'Test: Element: Radios'
 description: 'Test the radios.'
@@ -20,6 +22,7 @@ elements: |
     radios_required:
       '#type': radios
       '#title': radios_required
+      '#description': 'This is a description'
       '#required': true
       '#options': yes_no
   radios_required_conditional_example:
@@ -106,6 +109,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -113,6 +117,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -128,22 +133,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -154,6 +174,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -172,9 +193,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -227,6 +250,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_range.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_range.yml
index 7f110007f10c237ae9208bcef92ab9220b2ea7ce..f56719bcc2c70ed19a7822740b962d136de2ccd7 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_range.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_range.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_range
 title: 'Test: Element: Range'
 description: 'Test the range element.'
@@ -37,7 +39,7 @@ elements: |
     '#attributes':
       style: 'width: 500px'
     '#output': below
-    '#output__field_prefix': '$'
+    '#output__field_prefix': $
     '#output__field_suffix': '.00'
     '#output__attributes':
       style: 'background-color: yellow'
@@ -51,10 +53,10 @@ elements: |
     '#min': 0
     '#max': 10000
     '#step': 100
-    '#field_prefix': '$0.00'
+    '#field_prefix': $0.00
     '#field_suffix': '$10,000'
     '#output': left
-    '#output__field_prefix': '$'
+    '#output__field_prefix': $
     '#output__field_suffix': '.00'
     '#output__attributes':
       style: 'background-color: yellow'
@@ -76,6 +78,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -83,6 +86,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -98,22 +102,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -124,6 +143,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -142,9 +162,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -197,6 +219,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_rating.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_rating.yml
index 2da2ddac0b27cb25ef02ef874ccf8dc17fb9f117..696396349249b7458824c57628b9709c468f8880 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_rating.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_rating.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_rating
 title: 'Test: Element: Rating'
 description: 'Test the rating element.'
@@ -19,14 +21,18 @@ elements: |
     '#open': true
     rating_basic:
       '#type': webform_rating
-      '#title': 'Rating basic'
+      '#title': rating_basic
     rating_advanced:
       '#type': webform_rating
-      '#title': 'Rating advanced'
+      '#title': rating_advanced
       '#star_size': large
       '#reset': true
       '#max': '10'
       '#step': '0.1'
+    rating_required:
+      '#type': webform_rating
+      '#title': rating_required
+      '#required': true
 css: ''
 javascript: ''
 settings:
@@ -35,6 +41,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -42,6 +49,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -57,22 +65,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -83,6 +106,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -101,9 +125,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -156,6 +182,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_readonly.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_readonly.yml
index b9fb9ab280d8800e0a4bfdca0c61f9f0f91297be..81077f627fe8d1a9410779dfb4749036255a2925 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_readonly.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_readonly.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_readonly
 title: 'Test: Element: Read-only'
 description: 'Test read-only elements.'
@@ -29,6 +31,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -36,6 +39,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -51,22 +55,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -77,6 +96,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -95,9 +115,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -150,6 +172,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_section.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_section.yml
index f49779b99308ddf62c892bc032df06a47723480e..6bc5bcaa0d70d36b13c8480cd3649e86c66c2733 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_section.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_section.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_section
 title: 'Test: Element: Section'
 description: 'Test the section element.'
@@ -16,27 +18,22 @@ elements: |
   webform_section:
     '#type': webform_section
     '#title': webform_section
-    '#help': '{This is help text}'
-    '#description': '{This is a description}'
-    webform_section_textfield:
-      '#type': textfield
-      '#title': webform_section_textfield
-  webform_section_required:
-    '#type': webform_section
-    '#title': webform_section_required
+    '#description': 'This is a description.'
+    '#help': 'This is help text.'
+    '#more': 'This is more text'
     '#required': true
-    webform_section_required_textfield:
-      '#type': textfield
-      '#title': webform_section_required_textfield
+    webform_section_markup:
+      '#markup': '<p>This is some markup</p>'
+  webform_section_title_invisible:
+    '#type': webform_section
+    '#title': webform_section_title_invisible
+    '#title_display': invisible
   webform_section_title_custom:
     '#type': webform_section
     '#title': webform_section_title_custom
     '#title_tag': h5
     '#title_attributes':
-      'style': 'color: red'
-    webform_section_h5_textfield:
-      '#type': textfield
-      '#title': webform_section_h5_textfield
+      style: 'color: red'
 css: ''
 javascript: ''
 settings:
@@ -45,6 +42,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -52,6 +50,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -67,22 +66,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -93,6 +107,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -111,9 +126,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -166,4 +183,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_select.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_select.yml
index 821d3373f4e7aac25d67dcdf955670f172a7194a..f0f16129461aac3a566b64d6c825596fa440c912 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_select.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_select.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_select
 title: 'Test: Element: Select'
 description: 'Tests select menus.'
@@ -15,7 +17,7 @@ category: 'Test: Element'
 elements: |
   select_examples:
     '#type': details
-    '#title': 'Select'
+    '#title': Select
     '#open': true
     select:
       '#type': select
@@ -50,6 +52,17 @@ elements: |
         three: Three
         four: Four
         five: Five
+    select_select2_placeholder:
+      '#type': select
+      '#title': select_select2_placeholder
+      '#select2': true
+      '#placeholder': 'This is a placeholder'
+      '#options':
+        one: One
+        two: Two
+        three: Three
+        four: Four
+        five: Five
     select_select2_multiple:
       '#type': select
       '#title': select_select2_multiple
@@ -61,6 +74,66 @@ elements: |
         three: Three
         four: Four
         five: Five
+    select_select2_multiple_placeholder:
+      '#type': select
+      '#title': select_select2_multiple_placeholder
+      '#select2': true
+      '#placeholder': 'This is a placeholder'
+      '#multiple': true
+      '#options':
+        one: One
+        two: Two
+        three: Three
+        four: Four
+        five: Five
+  select_chosen_examples:
+    '#type': details
+    '#title': 'Select chosen'
+    '#open': true
+    select_chosen:
+      '#type': select
+      '#title': select_chosen
+      '#chosen': true
+      '#options':
+        one: One
+        two: Two
+        three: Three
+        four: Four
+        five: Five
+    select_chosen_placeholder:
+      '#type': select
+      '#title': select_chosen_placeholder
+      '#chosen': true
+      '#placeholder': 'This is a placeholder'
+      '#options':
+        one: One
+        two: Two
+        three: Three
+        four: Four
+        five: Five
+    select_chosen_multiple:
+      '#type': select
+      '#title': select_chosen_multiple
+      '#chosen': true
+      '#multiple': true
+      '#options':
+        one: One
+        two: Two
+        three: Three
+        four: Four
+        five: Five
+    select_chosen_multiple_placeholder:
+      '#type': select
+      '#title': select_chosen_multiple_placeholder
+      '#chosen': true
+      '#placeholder': 'This is a placeholder'
+      '#multiple': true
+      '#options':
+        one: One
+        two: Two
+        three: Three
+        four: Four
+        five: Five
   select_empty_option_examples:
     '#type': details
     '#title': 'Select empty option'
@@ -138,6 +211,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -145,6 +219,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -160,22 +235,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -186,6 +276,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -204,9 +295,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -259,6 +352,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_signature.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_signature.yml
index 8f42ac289a84532606fc2ff5ab6699e54e08d59b..f9d9f08785ea351a9f79b116228f5ed12407e371 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_signature.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_signature.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_signature
 title: 'Test: Element: Signature'
 description: 'Test the signature element.'
@@ -24,6 +26,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -31,6 +34,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -46,22 +50,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -72,6 +91,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -90,9 +110,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -145,6 +167,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_states.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_states.yml
index e253544498526caf5c704d9ec0694759b13efddc..e3fd7a63227a149737ddc73ada1eb1748e9f637b 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_states.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_states.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_states
 title: 'Test: Element: #States API'
 description: 'Test Drupal''s #states API states (visible, required, disabled, checked, and expanded).'
@@ -20,6 +22,16 @@ elements: |
       selector_01: 'Selector 01 (selector_01)'
       selector_02: 'Selector 02 (selector_02)'
       selector_03: 'Selector 03 (selector_03)'
+    '#selector_sources':
+      selector_01:
+        option_a: 'Option A'
+        option_b: 'Option B'
+      selector_02:
+        option_c: 'Option C'
+        option_d: 'Option D'
+      selector_03:
+        option_e: 'Option E'
+        option_f: 'Option F'
     '#default_value':
       enabled:
         selector_01:
@@ -119,6 +131,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -126,6 +139,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -141,22 +155,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -167,6 +196,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -185,9 +215,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -240,6 +272,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_submission_views.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_submission_views.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b28fe93852569e754e47b3efaca293f13455aade
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_submission_views.yml
@@ -0,0 +1,210 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_submission_views
+title: 'Test: Element: Webform Submission Views'
+description: 'Test the webform submission views.'
+category: 'Test: Element'
+elements: |
+  global:
+    '#type': details
+    '#title': global
+    '#open': true
+    webform_submission_views_global:
+      '#type': webform_submission_views
+      '#global': true
+      '#default_value':
+        admin:
+          view: 'webform_submissions:embed_administer'
+          title: Admin
+          global_routes:
+            - entity.webform_submission.collection
+          webform_routes:
+            - entity.webform.results_submissions
+          node_routes:
+            - entity.node.webform.results_submissions
+  webform:
+    '#type': details
+    '#title': webform
+    '#open': true
+    webform_submission_views:
+      '#type': webform_submission_views
+      '#default_value':
+        admin:
+          view: 'webform_submissions:embed_administer'
+          title: Admin
+          webform_routes:
+            - entity.webform.results_submissions
+          node_routes:
+            - entity.node.webform.results_submissions
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: true
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 1
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_submission_views_r.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_submission_views_r.yml
new file mode 100644
index 0000000000000000000000000000000000000000..af95ca7357062c7c09682edefa76628a94d5a6c3
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_submission_views_r.yml
@@ -0,0 +1,204 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_submission_views_r
+title: 'Test: Element: Webform Submission Views replace'
+description: 'Test the webform submission views replace.'
+category: 'Test: Element'
+elements: |
+  global:
+    '#type': details
+    '#title': global
+    '#open': true
+    webform_submission_views_replace_global:
+      '#type': webform_submission_views_replace
+      '#global': true
+      '#default_value':
+        global_routes:
+          - entity.webform_submission.collection
+        webform_routes:
+          - entity.webform.results_submissions
+        node_routes:
+          - entity.node.webform.results_submissions
+  webform:
+    '#type': details
+    '#title': webform
+    '#open': true
+    webform_submission_views_replace:
+      '#type': webform_submission_views_replace
+      '#default_value':
+        webform_routes:
+          - entity.webform.results_submissions
+        node_routes:
+          - entity.node.webform.results_submissions
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: true
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 1
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_submitted_value.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_submitted_value.yml
index 523a521554bb582f4037c4c6726a1f3941c541aa..a2ff6f9aa67a659c39a2e5bec2e225ad5c0a5860 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_submitted_value.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_submitted_value.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_submitted_value
 title: 'Test: Element: Submitted value'
 description: 'Test submitted value handling for options element.'
@@ -60,6 +62,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -67,6 +70,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -82,22 +86,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -108,6 +127,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -126,9 +146,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -181,6 +203,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_table.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_table.yml
index 8514ed6ea431ed55a72f310b9f058fff19dbf765..aedd2c51dc5ff87d2a0ee7eb1f4a31ee346750a9 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_table.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_table.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_table
 title: 'Test: Element: Table'
 description: 'Test table elements.'
@@ -189,6 +191,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -196,6 +199,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -211,22 +215,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -237,6 +256,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -255,9 +275,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -310,6 +332,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_telephone.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_telephone.yml
index 0ef3ccd204c3a4c3324d0dbec67ffd08749a8685..df38050aeb1baf70aa011280df7b779404f5bf28 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_telephone.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_telephone.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_telephone
 title: 'Test: Element: Telephone'
 description: 'Test telephone element.'
@@ -20,6 +22,16 @@ elements: |
     '#type': tel
     '#title': 'tel international'
     '#international': true
+  tel_validation_e164:
+    '#type': tel
+    '#title': tel_validation_e164
+    '#telephone_validation_format': '0'
+  tel_validation_national:
+    '#type': tel
+    '#title': tel_validation_national
+    '#description': 'United States - +1'
+    '#telephone_validation_format': '2'
+    '#telephone_validation_country': US
 css: ''
 javascript: ''
 settings:
@@ -28,6 +40,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -35,6 +48,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -50,22 +64,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -76,6 +105,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -94,9 +124,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -149,6 +181,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_term_reference.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_term_reference.yml
index cec6b9a2a4a7c42d7bdc850fdaadd99474759761..01d6229b8bf775c08105b980690a6da140741f14 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_term_reference.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_term_reference.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_term_reference
 title: 'Test: Element: Term reference'
 description: 'Test term reference element.'
@@ -30,7 +32,7 @@ elements: |
         '#title': webform_term_checkboxes_tree_advanced
         '#vocabulary': tags
         '#multiple': true
-        '#tree_delimiter': '..'
+        '#tree_delimiter': ..
         '#scroll': false
         '#format': breadcrumb
         '#format_items': ul
@@ -71,7 +73,7 @@ elements: |
         '#title': webform_term_select_tree_advanced
         '#vocabulary': tags
         '#multiple': true
-        '#tree_delimiter': '..'
+        '#tree_delimiter': ..
         '#format': breadcrumb
         '#format_items': ul
     webform_term_select_breadcrumb:
@@ -102,6 +104,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -109,6 +112,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -124,22 +128,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -150,6 +169,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -168,9 +188,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -223,4 +245,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_terms_of_service.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_terms_of_service.yml
index c0f6da83b06af6c62d300442cfd4b09434deedd6..3e2d84a223e03cdab0b157319f416fc42b8b8a20 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_terms_of_service.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_terms_of_service.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_terms_of_service
 title: 'Test: Element: Terms of Service'
 description: 'Test terms of service element.'
@@ -15,19 +17,19 @@ category: 'Test: Element'
 elements: |
   terms_of_service_default:
     '#type': webform_terms_of_service
-    '#title': I agree to the {terms of service}. (default)
+    '#title': 'I agree to the {terms of service}. (default)'
     '#terms_title': terms_of_service_default
     '#terms_content': 'These are the terms of service.'
     '#required': true
   terms_of_service_modal:
     '#type': webform_terms_of_service
-    '#title': I agree to the {terms of service}. (modal)
+    '#title': 'I agree to the {terms of service}. (modal)'
     '#terms_type': modal
     '#terms_title': terms_of_service_modal
     '#terms_content': 'These are the terms of service.'
   terms_of_service_slideout:
     '#type': webform_terms_of_service
-    '#title': I agree to the {terms of service}. (slideout)
+    '#title': 'I agree to the {terms of service}. (slideout)'
     '#terms_type': slideout
     '#terms_title': terms_of_service_slideout
     '#terms_content': 'These are the terms of service.'
@@ -39,6 +41,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -46,6 +49,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -61,22 +65,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -87,6 +106,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -105,9 +125,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -160,4 +182,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_text_format.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_text_format.yml
index b5dfe3f073b7a5432af7d00ed08018f6e0036893..c9386c2e65a798cdd488f86368208d811d168870 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_text_format.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_text_format.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_text_format
 title: 'Test: Element: Text format'
 description: 'Test text webform element support.'
@@ -18,6 +20,7 @@ elements: |
     '#title': text_format
     '#format': plain_text
     '#default_value': 'The quick brown fox jumped over the lazy dog.'
+    '#hide_help': true
 css: ''
 javascript: ''
 settings:
@@ -26,6 +29,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +37,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +53,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +94,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -92,9 +113,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -147,4 +170,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_time.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_time.yml
index 20343a74708e8f7f432f21acc59a2cf4e0741fd2..086c14860905b4340f1d4ee2b4ecda450b331efd 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_time.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_time.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_time
 title: 'Test: Element: Time'
 description: 'Test time element.'
@@ -68,6 +70,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -75,6 +78,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -90,22 +94,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -116,6 +135,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -134,9 +154,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -189,4 +211,16 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
-handlers: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 1
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_toggle.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_toggle.yml
index e5c2d64e48aae0e364b72701cbff810e165714be..58438812e0f1653ca5bdd62a7bae989fd7fd46eb 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_toggle.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_toggle.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_toggle
 title: 'Test: Element: Toggle'
 description: 'Test the toggle element.'
@@ -55,6 +57,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -62,6 +65,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -77,22 +81,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -103,6 +122,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -121,9 +141,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -176,6 +198,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_users_roles.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_users_roles.yml
index 105ee0b1e533f4daa3b7d013f66179061dd59d77..459eb1d9decf7a4eb1ad3d792feef8f2ed4473f2 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_users_roles.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_users_roles.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_users_roles
 title: 'Test: Element: Users & Roles'
 description: 'Test users and roles element.'
@@ -38,6 +40,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -45,6 +48,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -60,22 +64,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -86,6 +105,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -104,9 +124,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -159,6 +181,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_minlength.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_minlength.yml
index 0806249837391b705de94c567af32fcede83e1f5..d96b01a91446670e410c281f1a45f540d65c767c 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_minlength.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_minlength.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_validate_minlength
 title: 'Test: Element: Validate Minlength'
 description: 'Test #minlength element validation support.'
@@ -18,6 +20,12 @@ elements: |
     '#title': minlength_textfield
     '#minlength': 5
     '#default_value': value
+  minlength_textfield_required:
+    '#type': textfield
+    '#title': minlength_textfield_required
+    '#minlength': 5
+    '#required': true
+    '#default_value': value
 css: ''
 javascript: ''
 settings:
@@ -26,6 +34,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +42,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +58,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +99,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -92,9 +118,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -147,4 +175,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_multiple.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_multiple.yml
index 3751a3d1e59481e1f5fdb420650ab2be6b93b812..ee613047b1aad701a50bb1a867772493a6eedef2 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_multiple.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_multiple.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_validate_multiple
 title: 'Test: Element: Validate Multiple'
 description: 'Test #multiple element validation support.'
@@ -20,7 +22,7 @@ elements: |
     webform_element_multiple_textfield_unlimited:
       '#type': textfield
       '#title': webform_element_multiple_textfield_unlimited
-      '#multiple': TRUE
+      '#multiple': true
     webform_element_multiple_textfield_three:
       '#type': textfield
       '#title': webform_element_multiple_textfield_three
@@ -37,7 +39,7 @@ elements: |
     webform_element_multiple_link_unlimited:
       '#type': webform_link
       '#title': webform_element_multiple_link_unlimited
-      '#multiple': TRUE
+      '#multiple': true
       '#multiple__header': true
   checkboxes:
     '#type': details
@@ -84,6 +86,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -91,6 +94,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -106,22 +110,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -132,6 +151,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -150,9 +170,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -205,6 +227,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_required.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_required.yml
index d676d173128ecccd34a748d1d1373b56bce6786e..317dda81dc4b784927e24cd7aeb82363c77122e9 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_required.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_required.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_validate_required
 title: 'Test: Element: Validate Required'
 description: 'Test #required_error element validation support.'
@@ -35,6 +37,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -42,6 +45,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -57,22 +61,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -83,6 +102,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -101,9 +121,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -156,4 +178,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_unique.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_unique.yml
index 5d45926935e9a7d85cd5ede888360b553660611d..495a2b1d4d9e3e29628a9e38ecea0927aab50129 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_unique.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_element_validate_unique.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_validate_unique
 title: 'Test: Element: Validate Unique'
 description: 'Test #unique element validation support.'
@@ -40,17 +42,14 @@ elements: |
     '#title': unique_error
     '#unique': true
     '#unique_error': 'unique_error error message.'
-  unique_ignored:
+  unique_multiple:
     '#type': checkboxes
-    '#title': unique_ignored
+    '#title': unique_multiple
     '#unique': true
-    '#unique_error': 'unique_ignored error message.'
+    '#unique_error': 'unique_multiple error message.'
     '#options':
       1: one
       2: two
-    '#default_value':
-      - 1
-      - 2
 css: ''
 javascript: ''
 settings:
@@ -59,6 +58,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -66,6 +66,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: true
@@ -81,22 +82,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -107,6 +123,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -125,9 +142,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -180,4 +199,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_example_elements.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_example_elements.yml
index c468a0efae583dd6efdac3e408890cbf75136eff..978f1e986e2cf058db47c9b0e268b6e97244aab0 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_example_elements.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_example_elements.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_example_elements
 title: 'Test: Example: Elements'
 description: 'Examples of every supported <a href="https://api.drupal.org/api/drupal/elements">webform element</a>.'
@@ -98,7 +100,7 @@ elements: |
       '#min': 0
       '#max': 100
       '#step': 1
-      '#output': right
+      '#output': below
       '#output__field_prefix': $
       '#output__field_suffix': '.00'
     webform_rating:
@@ -158,9 +160,6 @@ elements: |
       '#message_type': warning
       '#message_message': 'This is a <strong>warning</strong> message.'
       '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/77245">Issue #77245: A place for JavaScript status messages</a>'
-    table:
-      '#type': table
-      '#title': Table
   file_upload_elements:
     '#type': details
     '#title': 'File upload elements'
@@ -350,11 +349,11 @@ elements: |
     webform_computed_token:
       '#type': webform_computed_token
       '#title': 'Computed token'
-      '#value': 'This is a Computed token value.'
+      '#template': 'This is a Computed token value.'
     webform_computed_twig:
       '#type': webform_computed_twig
       '#title': 'Computed Twig'
-      '#value': 'This is a Computed Twig value.'
+      '#template': 'This is a Computed Twig value.'
   containers:
     '#type': details
     '#title': Containers
@@ -376,9 +375,6 @@ elements: |
       '#field_prefix': '{field_prefix}'
       '#field_suffix': '{field_suffix}'
       '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/783438">Issue #783438: #states doesn''t work for #type item</a>'
-    label:
-      '#type': label
-      '#title': Label
     webform_section:
       '#type': webform_section
       '#title': Section
@@ -502,6 +498,13 @@ elements: |
       '#vocabulary': tags
       '#multiple': true
       '#select2': true
+  markup:
+    '#type': details
+    '#title': Markup
+    '#open': true
+    label:
+      '#type': label
+      '#title': Label
   other_elements:
     '#type': details
     '#title': 'Other elements'
@@ -512,6 +515,9 @@ elements: |
     machine_name:
       '#type': machine_name
       '#title': 'Machine name'
+    table:
+      '#type': table
+      '#title': Table
 css: ''
 javascript: ''
 settings:
@@ -520,6 +526,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -527,6 +534,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -542,22 +550,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -568,6 +591,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -586,9 +610,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -641,4 +667,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_example_elements_composite.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_example_elements_composite.yml
index ca5328aa3a80cb25dc0abf7e98e4ca2677d68aeb..e8e5938e490f397b512d096913be1d4f86252c14 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_example_elements_composite.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_example_elements_composite.yml
@@ -6,31 +6,56 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_example_elements_composite
 title: 'Test: Example: Elements: Composite'
 description: 'Examples of composite elements, includes address, and contact.'
 category: 'Test: Example'
 elements: |
+  address_example:
+    '#type': details
+    '#title': 'Advanced address'
+    '#open': true
+    address:
+      '#type': address
+      '#title': 'Advanced address'
+    address_multiple:
+      '#type': address
+      '#title': 'Advanced address multiple'
+      '#multiple': true
   webform_address_example:
     '#type': details
-    '#title': Address
+    '#title': 'Basic address'
     '#open': true
     webform_address:
       '#type': webform_address
-      '#title': Address
+      '#title': 'Basic address'
     webform_address_multiple:
       '#type': webform_address
-      '#title': 'Address multiple'
+      '#title': 'Basic address multiple'
+      '#multiple': true
+      '#multiple__header': true
+  webform_contact_example:
+    '#type': details
+    '#title': Contact
+    '#open': true
+    webform_contact:
+      '#type': webform_contact
+      '#title': Contact
+    webform_contact_multiple:
+      '#type': webform_contact
+      '#title': 'Contact multiple'
       '#multiple': true
   webform_custom_composite_example:
     '#type': details
-    '#title': 'Composite custom'
+    '#title': 'Custom composite'
     '#open': true
     webform_custom_composite:
       '#type': webform_custom_composite
-      '#title': 'Composite custom'
+      '#title': 'Custom composite'
       '#element':
         name:
           '#type': textfield
@@ -45,7 +70,7 @@ elements: |
             Female: Female
     webform_custom_composite_multiple:
       '#type': webform_custom_composite
-      '#title': 'Composite custom multiple'
+      '#title': 'Custom composite multiple'
       '#element':
         name:
           '#type': textfield
@@ -60,17 +85,6 @@ elements: |
             Female: Female
       '#multiple': true
       '#multiple__header': true
-  webform_contact_example:
-    '#type': details
-    '#title': Contact
-    '#open': true
-    webform_contact:
-      '#type': webform_contact
-      '#title': Contact
-    webform_contact_multiple:
-      '#type': webform_contact
-      '#title': 'Contact multiple'
-      '#multiple': true
   webform_likert_example:
     '#type': details
     '#title': Likert
@@ -97,18 +111,19 @@ elements: |
       '#type': webform_link
       '#title': 'Link multiple'
       '#multiple': true
-  webform_location_example:
+      '#multiple__header': true
+  webform_location_geocomplete_example:
     '#type': details
     '#title': Location
     '#open': true
-    webform_location:
-      '#type': webform_location
+    webform_location_geocomplete:
+      '#type': webform_location_places
       '#title': Location
       '#map': true
       '#geolocation': true
       '#format': map
-    webform_location_multiple:
-      '#type': webform_location
+    webform_location_geocomplete_multiple:
+      '#type': webform_location_places
       '#title': 'Location multiple'
       '#map': true
       '#geolocation': true
@@ -140,6 +155,7 @@ elements: |
       '#type': webform_name
       '#title': 'Name multiple'
       '#multiple': true
+      '#multiple__header': true
   webform_telephone_example:
     '#type': details
     '#title': 'Telephone advanced'
@@ -151,6 +167,7 @@ elements: |
       '#type': webform_telephone
       '#title': 'Telephone advanced multiple'
       '#multiple': true
+      '#multiple__header': true
   text_format_example:
     '#type': details
     '#title': 'Text format'
@@ -167,6 +184,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -174,6 +192,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -189,22 +208,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -215,6 +249,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -233,9 +268,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -288,4 +325,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_exporter_entity_reference.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_exporter_entity_reference.yml
index 03dc5aa6310fd7bc46365375432be1ece76cffc7..f08dfb78b8253b130b9f4ec99f47faa41a393463 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_exporter_entity_reference.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_exporter_entity_reference.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_exporter_entity_reference
 title: 'Test: Exporter: Entity Reference'
 description: 'Test exporting entity references.'
@@ -39,6 +41,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -46,6 +49,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -61,22 +65,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -87,6 +106,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -105,9 +125,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -160,4 +182,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_exporter_options.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_exporter_options.yml
index 3841ba8d6bbf0ff0a91e3a4e42167c7f968fbebf..3a811b56fbf56325dbc0178df2c019051b92cf07 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_exporter_options.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_exporter_options.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_exporter_options
 title: 'Test: Exporter: Options'
 description: 'Test exporting options element.'
@@ -49,6 +51,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -56,6 +59,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -71,22 +75,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -97,6 +116,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -115,9 +135,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -170,4 +192,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_access_denied.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_access_denied.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bc4d2720311d42e0024e7241d3b682fcd4fcac26
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_access_denied.yml
@@ -0,0 +1,173 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_form_access_denied
+title: 'Test: Webform: Access Denied'
+description: 'Test access denied to form or submission.'
+category: 'Test: Webform'
+elements: |
+  textfield:
+    '#type': textfield
+    '#title': textfield
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: login
+  form_access_denied_title: 'Webform: Access denied'
+  form_access_denied_message: 'Please login to access <b>[webform:title]</b>.'
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: login
+  submission_access_denied_title: 'Webform submission: Access Denied'
+  submission_access_denied_message: 'Please login to access <b>[webform_submission:label]</b>.'
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_api.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_api.yml
index f421288c7eec11fee87946053d1361b3c988bbb9..b1ef23e857f738d5f793bea51ab2f418f5b551bc 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_api.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_api.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_api
 title: 'Test: Form API'
 description: 'Test Form API validation'
@@ -124,6 +126,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -131,6 +134,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -146,22 +150,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -172,6 +191,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -190,9 +210,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -245,4 +267,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_login.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_archived.yml
similarity index 74%
rename from web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_login.yml
rename to web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_archived.yml
index 2bf1e93343fd8873c4db4af262232be5cc0ca1eb..29e3e72e406449501618302776753756f1d2bd0a 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_login.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_archived.yml
@@ -6,16 +6,17 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
-id: test_form_login
-title: 'Test: Webform: Login'
-description: 'Test login access denied to form or submission.'
+archive: true
+id: test_form_archived
+title: 'Test: Webform: Archive'
+description: 'Test archiving a webform.'
 category: 'Test: Webform'
 elements: |
-  textfield:
-    '#type': textfield
-    '#title': textfield
+  description:
+    '#markup': 'This webform is archived.'
 css: ''
 javascript: ''
 settings:
@@ -24,6 +25,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -31,6 +33,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -46,22 +49,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: true
-  form_login_message: 'Please login to access <b>[webform:title]</b>.'
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: true
-  submission_login_message: 'Please login to access <b>[webform_submission:label]</b>.'
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -72,6 +90,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -90,9 +109,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -145,4 +166,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_assets.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_assets.yml
index ecb1d94284f9e3811dfa4eba80f7ae446992d089..f65fdad01c1145e5066c09db779e6a45dd2ce55e 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_assets.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_assets.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_assets
 title: 'Test: Webform: Assets (CSS/JS)'
 description: 'Test custom CSS and JS webform assets.'
@@ -22,18 +24,21 @@ css: |
     display: none;
     padding: 20px;
   }
+  
 javascript: |
   (function ($) {
     $(function() {
       $('.webform-submission-form').fadeIn(3000);
     });
   })(jQuery);
+  
 settings:
   ajax: false
   ajax_scroll_top: form
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -41,6 +46,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -56,22 +62,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -82,6 +103,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -100,9 +122,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -155,4 +179,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_autofill.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_autofill.yml
index ada1c6ff12d7636d5ab50037099084db661a0350..dbc1d7f3246c8c4a070eba7462f24aaa415ab5ce 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_autofill.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_autofill.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_autofill
 title: 'Test: Webform: Autofill'
 description: 'Test autofill with previous submission data.'
@@ -27,6 +29,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -34,6 +37,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -49,15 +53,28 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: true
   autofill_message: ''
   autofill_excluded_elements:
@@ -65,7 +82,9 @@ settings:
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -76,6 +95,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: all
   draft_multiple: false
   draft_auto_save: false
@@ -94,9 +114,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -149,4 +171,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_autofocus.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_autofocus.yml
index 62140a777d2f90e6748c3c2ffae21d3680eebed7..ca1ec6b63624bb94bc3186bcfe7840578e221588 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_autofocus.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_autofocus.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_autofocus
 title: 'Test: Webform: Autofocus'
 description: 'Test autofocusing the first element.'
@@ -24,6 +26,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -31,6 +34,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -46,22 +50,37 @@ settings:
   form_submit_back: false
   form_autofocus: true
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -72,6 +91,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -90,9 +110,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -145,4 +167,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_closed.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_closed.yml
index 962f61a226288ba62277f4cbb87d9b12a4ad57e6..6136e0710cce094152b1de3b5d355013f6b6f317 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_closed.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_closed.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_closed
 title: 'Test: Webform: Closed'
 description: 'Test warning messages being displayed when a webform is closed.'
@@ -23,6 +25,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -30,6 +33,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -45,22 +49,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -71,6 +90,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -89,9 +109,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -144,4 +166,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_confidential.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_confidential.yml
index 23244206e5396cc6735715c66d2e174a84a9dec8..4dc64c243af4a56f1cbacb9009a6fe11202f1216 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_confidential.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_confidential.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_confidential
 title: 'Test: Webform: Confidential'
 description: 'Test confidential webforms.'
@@ -24,6 +26,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -31,6 +34,7 @@ settings:
   form_previous_submissions: true
   form_confidential: true
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -46,22 +50,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -72,6 +91,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -90,9 +110,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -145,4 +167,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_details_toggle.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_details_toggle.yml
index 1350ce83e3d5d8d764c97c7c1f62427a1275564a..d8f3eea4128c48603f35eae1720d90e61862c55e 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_details_toggle.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_details_toggle.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_details_toggle
 title: 'Test: Webform: Details toggle'
 description: 'Test details expand/collapse all toggle link.'
@@ -31,6 +33,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -38,6 +41,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -53,22 +57,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: true
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -79,6 +98,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -97,9 +117,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -152,4 +174,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_disable_autocomplete.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_disable_autocomplete.yml
index 7659061193169671c82236dbfab678a6190338b5..373e9751fef154f6a9efa1b27cbecd91a06f0aae 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_disable_autocomplete.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_disable_autocomplete.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_disable_autocomplete
 title: 'Test: Webform: Disable autocomplete'
 description: 'Test disabling autocompletion for all elements.'
@@ -26,6 +28,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +36,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +52,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +93,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -92,9 +112,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -147,4 +169,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_disable_back.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_disable_back.yml
index e8f5eecbc97a7df740a7913fc13b5b9573c7c0e2..c455bdf9293e040cbf4ff3592ab0ed8bf84bc16e 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_disable_back.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_disable_back.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_disable_back
 title: 'Test: Webform: Disabled back button'
 description: 'Test disabling the ability to navigate back using the back button.'
@@ -351,6 +353,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -358,6 +361,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -373,22 +377,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: true
   wizard_progress_percentage: true
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -399,6 +418,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -417,9 +437,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -472,4 +494,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_disable_inline_errors.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_disable_inline_errors.yml
index f5e8d5385d64d1740e4e5640af8b79fd5e52214b..8be662a991ee13d7926a9e0e47f18b05de8cd285 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_disable_inline_errors.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_disable_inline_errors.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_disable_inline_errors
 title: 'Test: Webform: Disable inline form errors'
 description: 'Test disabling inline form errors.'
@@ -25,6 +27,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -32,6 +35,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -47,22 +51,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -73,6 +92,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -91,9 +111,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -146,4 +168,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_draft_anonymous.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_draft_anonymous.yml
index 82ad2e414b36f4e33a23e8ba2c76342798881e06..a7bc5fd846e71d3fcb1b688ab6eb339dee05a241 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_draft_anonymous.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_draft_anonymous.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_draft_anonymous
 title: 'Test: Webform: Draft anonymous'
 description: 'Test saving a draft and previewing a submission for anonymous (and authenticated users).'
@@ -28,6 +30,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -35,12 +38,13 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: true
   form_prepopulate: true
   form_prepopulate_source_entity: false
   form_prepopulate_source_entity_required: false
   form_prepopulate_source_entity_type: ''
-  form_reset: false
+  form_reset: true
   form_disable_autocomplete: false
   form_novalidate: false
   form_disable_inline_errors: false
@@ -50,22 +54,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -76,6 +95,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: all
   draft_multiple: false
   draft_auto_save: true
@@ -94,9 +114,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -149,4 +171,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_draft_authenticated.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_draft_authenticated.yml
index dcdec66792ed9f098b18f5cdb2e62c13a256edc0..71d3caaf850c7b32427a4258c0d0cf1d3ef9de85 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_draft_authenticated.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_draft_authenticated.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_draft_authenticated
 title: 'Test: Webform: Draft authenticated'
 description: 'Test saving a draft and previewing a submission for authenticated users.'
@@ -28,6 +30,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -35,12 +38,13 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: true
   form_prepopulate_source_entity: false
   form_prepopulate_source_entity_required: false
   form_prepopulate_source_entity_type: ''
-  form_reset: false
+  form_reset: true
   form_disable_autocomplete: false
   form_novalidate: false
   form_disable_inline_errors: false
@@ -50,22 +54,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -76,6 +95,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: authenticated
   draft_multiple: false
   draft_auto_save: true
@@ -94,9 +114,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -149,4 +171,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_draft_multiple.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_draft_multiple.yml
index 2e839d9dfc14d1227674125e1c5a57c53902f620..b18c8140f6c1dea3322c81146e77f01f00e1df46 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_draft_multiple.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_draft_multiple.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_draft_multiple
 title: 'Test: Webform: Draft multiple'
 description: 'Test saving multiple drafts.'
@@ -28,6 +30,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -35,12 +38,13 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: true
   form_prepopulate_source_entity: false
   form_prepopulate_source_entity_required: false
   form_prepopulate_source_entity_type: ''
-  form_reset: false
+  form_reset: true
   form_disable_autocomplete: false
   form_novalidate: false
   form_disable_inline_errors: false
@@ -50,22 +54,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -76,6 +95,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: all
   draft_multiple: true
   draft_auto_save: true
@@ -94,9 +114,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -149,4 +171,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_inline_errors.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_inline_errors.yml
index ecf441fb217b8faacd28bc342036f08f7cc92179..d0f52342d07ae8a1f2d69b49abcf054e961f2de0 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_inline_errors.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_inline_errors.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_inline_errors
 title: 'Test: Webform: Inline Form Errors'
 description: 'Test inline form errors.'
@@ -132,6 +134,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -139,6 +142,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -154,22 +158,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -180,6 +199,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -198,9 +218,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -253,4 +275,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_limit.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_limit.yml
index 869fb3be84f9acec6f5c737effd3bf9c015b9dc5..3ca896ed0750d3f5af14ec131e3698b300acb1e4 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_limit.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_limit.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_limit
 title: 'Test: Form: Submission limit'
 description: 'Test submission and user limits.'
@@ -24,6 +26,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -31,6 +34,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: true
   form_prepopulate_source_entity: false
@@ -46,22 +50,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -72,6 +91,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: authenticated
   draft_multiple: false
   draft_auto_save: false
@@ -90,9 +110,11 @@ settings:
   limit_total: 4
   limit_total_interval: 60
   limit_total_message: 'Only 4 submissions are allowed.'
+  limit_total_unique: false
   limit_user: 1
   limit_user_interval: 60
   limit_user_message: 'You are only allowed to have 1 submission for this webform.'
+  limit_user_unique: false
   entity_limit_total: 2
   entity_limit_total_interval: 60
   entity_limit_user: 1
@@ -145,4 +167,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_limit_total_unique.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_limit_total_unique.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1062f8f5e8e7c9467390049bc62ff605660f9851
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_limit_total_unique.yml
@@ -0,0 +1,174 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_form_limit_total_unique
+title: 'Test: Form: Submission limit total unique'
+description: 'Test submission limit total unique.'
+category: 'Test: Form'
+elements: |
+  name:
+    '#type': textfield
+    '#title': Name
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: both
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: true
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: authenticated
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: true
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_limit_user_unique.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_limit_user_unique.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7e42cc8da0d83eb3efe7df28e23589aaccf221ac
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_limit_user_unique.yml
@@ -0,0 +1,174 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_form_limit_user_unique
+title: 'Test: Form: Submission limit user unique'
+description: 'Test submission limit user unique.'
+category: 'Test: Form'
+elements: |
+  name:
+    '#type': textfield
+    '#title': Name
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: both
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: true
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: authenticated
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: true
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_long_100.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_long_100.yml
index 4e293d5260f8da7e3159c0d1451dbdc8e5d60b09..c48ae3b4e20c9e1f046e53bab1dbaab4baf5d2ea 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_long_100.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_long_100.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_long_100
 title: 'Test: Webform: Long - 100 elements'
 description: 'Test webform with 100 elements'
@@ -321,6 +323,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -328,6 +331,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -343,22 +347,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -369,6 +388,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -387,9 +407,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -442,4 +464,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_long_200.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_long_200.yml
index 7adb8d58be1688cf79b04fabbd681f77f99c8025..b940c852c047c82933b0cf12c493b692907f3791 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_long_200.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_long_200.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_long_200
 title: 'Test: Webform: Long - 200 elements'
 description: 'Test webform with 200 elements'
@@ -621,6 +623,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -628,6 +631,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -643,22 +647,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -669,6 +688,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -687,9 +707,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -742,4 +764,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_long_300.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_long_300.yml
index 7866475ed29531fde34e980ae418addd69da0e2e..bc77082abc0e268c2b84f17a2fcd70b25ec30885 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_long_300.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_long_300.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_long_300
 title: 'Test: Webform: Long - 300 elements'
 description: 'Test webform with 300 elements'
@@ -921,6 +923,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -928,6 +931,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -943,22 +947,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -969,6 +988,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -987,9 +1007,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -1042,4 +1064,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_novalidate.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_novalidate.yml
index 8df5fc7f3dc2ffe677fbeec44c5b36d76e69b064..ba31ca3803116522db572baa5a2758269d7f4df7 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_novalidate.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_novalidate.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_novalidate
 title: 'Test: Webform: Novalidate'
 description: 'Test disabling client-side validation.'
@@ -28,6 +30,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -35,6 +38,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -50,22 +54,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -76,6 +95,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -94,9 +114,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -149,4 +171,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_opening.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_opening.yml
index b9dc93f376032d61472f597aca76cfd3e205b0e4..e235d6e573058ce8546622a0eaf225358d656f14 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_opening.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_opening.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: '2020-01-01T00:00:00-05:00'
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_opening
 title: 'Test: Webform: Opening'
 description: 'Test message being displayed when a webform is scheduled to open.'
@@ -23,6 +25,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: 'This form is opening soon.'
@@ -30,6 +33,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -45,22 +49,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -71,6 +90,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -89,9 +109,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -144,4 +166,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_prepopulate.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_prepopulate.yml
index 6684109223354838b33dff3f05227d3527523963..1bd7e936f462faa18005cab6bbfa0fd3e95afd2f 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_prepopulate.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_prepopulate.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_prepopulate
 title: 'Test: Webform: Prepopulate'
 description: 'Test prepopulating all form elements using query string parameters.'
@@ -33,6 +35,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -40,6 +43,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: true
   form_prepopulate_source_entity: true
@@ -55,22 +59,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -81,6 +100,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -99,9 +119,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -154,4 +176,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_preview.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_preview.yml
index e5a3c758ea6e2cba64ea1fb65cdb3d0f195cdbcb..f8f70625ae3b14f2f9447b6de28c220bbd1a115c 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_preview.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_preview.yml
@@ -6,20 +6,57 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_preview
 title: 'Test: Webform: Preview'
 description: 'Test submission preview.'
 category: 'Test: Webform'
 elements: |
-  name:
-    '#type': textfield
-    '#title': Name
-  email:
-    '#type': email
-    '#title': Email
-css: ''
+  fieldset:
+    '#type': fieldset
+    '#title': fieldset
+    '#format_attributes':
+      class:
+        - format-attributes-class
+    name:
+      '#type': textfield
+      '#title': Name
+      '#format_attributes':
+        class:
+          - format-attributes-class
+  container:
+    '#type': container
+    '#title': container
+    '#format_attributes':
+      class:
+        - format-attributes-class
+    email:
+      '#type': email
+      '#title': Email
+      '#format_attributes':
+        class:
+          - format-attributes-class
+  section:
+    '#type': webform_section
+    '#title': section
+    '#format_attributes':
+      class:
+        - format-attributes-class
+    checkbox:
+      '#type': checkbox
+      '#title': Checkbox
+      '#format_attributes':
+        class:
+          - format-attributes-class
+css: |
+  .format-attributes-class {
+    border: 1px dashed #ccc;
+    padding: 10px;
+  }
+  
 javascript: ''
 settings:
   ajax: false
@@ -27,6 +64,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -34,6 +72,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: true
   form_prepopulate_source_entity: false
@@ -49,22 +88,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: true
+  submission_exclude_empty_checkbox: true
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -75,6 +129,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: true
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -93,9 +148,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -148,4 +205,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_properties.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_properties.yml
index 641c87c1902446a3d675966a813d3d5f7bd16131..7f73e06ead64e584a8d0dce92c808761f541bb0f 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_properties.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_properties.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_properties
 title: 'Test: Webform: Properties'
 description: 'Test custom webform properties.'
@@ -36,6 +38,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -43,6 +46,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: true
   form_prepopulate_source_entity: false
@@ -58,22 +62,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -84,6 +103,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -102,9 +122,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -157,4 +179,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_remote_addr.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_remote_addr.yml
new file mode 100644
index 0000000000000000000000000000000000000000..566c2f6a85f1ca5aed2dfcb142a56b66f753e001
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_remote_addr.yml
@@ -0,0 +1,174 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_form_remote_addr
+title: 'Test: Webform: Remote IP Address'
+description: 'Test disabling the tracking of remote IP address.'
+category: 'Test: Webform'
+elements: |
+  name:
+    '#type': textfield
+    '#title': Name
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: false
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_required.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_required.yml
index e5a9e965754780f7c840ea4446c6c8bf4f882c94..e450acd3b2e50fe42d239e2ebe93248e106ed9cf 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_required.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_required.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_required
 title: 'Test: Webform: Required indicator'
 description: 'Test required element indicator.'
@@ -25,6 +27,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -32,6 +35,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -47,22 +51,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -73,6 +92,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -91,9 +111,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -146,4 +168,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_reset.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_reset.yml
index 63145dffc7abdf7e24431660fd98e39ed0aed624..e6a329fa9d3f127afdf8cfa5009fbbf0c205c41a 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_reset.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_reset.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_reset
 title: 'Test: Webform: Reset'
 description: 'Test form reset button.'
@@ -25,6 +27,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -32,6 +35,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -47,22 +51,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -73,6 +92,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -91,9 +111,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -146,4 +168,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_results_disabled.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_results_disabled.yml
index 0e10e2b19e8f5826c171a69efc534355669152f7..79bf87a2978cbe13b38ecc5d13bdbad3a713e6be 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_results_disabled.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_results_disabled.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_results_disabled
 title: 'Test: Form: Results Disabled'
 description: 'Test the disabling of saving of results to the database.'
@@ -23,6 +25,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -30,6 +33,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -45,22 +49,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -71,6 +90,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -89,9 +109,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -144,4 +166,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_submit_back.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_submit_back.yml
index a6529dea6ce20a54d14e7543e7c2599e991e9d39..a5e2438ac137f78dc8501c86137dfd51c1130ba9 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_submit_back.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_submit_back.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_submit_back
 title: 'Test: Webform: Back Button submit'
 description: 'Browser Back Button to submit to previous page.'
@@ -35,6 +37,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -42,6 +45,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -57,22 +61,37 @@ settings:
   form_submit_back: true
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -83,6 +102,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -101,9 +121,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -156,4 +178,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_submit_once.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_submit_once.yml
index d1c3e1a1fd21b96c565bfe54806a3e64e74d6f8a..acc0cf335e6a5eda069981c7587436f6e7e85382 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_submit_once.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_submit_once.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_submit_once
 title: 'Test: Webform: Submit once'
 description: 'Test submit once.'
@@ -32,6 +34,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: true
   form_exception_message: ''
   form_open_message: ''
@@ -39,6 +42,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -54,22 +58,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: true
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -80,6 +99,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: authenticated
   draft_multiple: false
   draft_auto_save: false
@@ -98,9 +118,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -153,4 +175,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_submit_text.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_submit_text.yml
index e2f303dd6c8df397bfcd371890d4f53c07b47d49..e37e26362fdd953450879bbd08e23d1bee640e64 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_submit_text.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_submit_text.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_submit_text
 title: 'Test: Webform: Submit text'
 description: 'Test customizing the webform''s submit button label.'
@@ -25,6 +27,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -32,6 +35,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -47,22 +51,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -73,6 +92,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -91,9 +111,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -146,4 +168,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_template.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_template.yml
index 4b76816bb46ea7869868b1d0e1d405312d58b90b..de685d68e0a577388830f81811a43b4bfbbe0c31 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_template.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_template.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: true
+archive: false
 id: test_form_template
 title: 'Test: Webform: Template'
 description: 'Test using a webform as a template.'
@@ -23,6 +25,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -30,6 +33,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -45,22 +49,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -71,6 +90,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -89,9 +109,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -144,4 +166,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_unsaved.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_unsaved.yml
index b6290784804609d16fff0539a93fbb4fc851fbef..5acbb9a1ad0f9dfdb3b43cc102b559d0fcca8366 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_unsaved.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_unsaved.yml
@@ -6,17 +6,23 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_unsaved
 title: 'Test: Webform: Unsaved'
 description: 'Test displaying unsaved data warning.'
 category: 'Test: Webform'
 elements: |
+  markup:
+    '#markup': 'Enter some data and hit reload'
   test:
     '#type': textfield
-    '#title': 'Text field'
-    '#description': 'Enter some data and hit reload'
+    '#title': textfield
+  text_format:
+    '#type': text_format
+    '#title': text_format
 css: ''
 javascript: ''
 settings:
@@ -25,6 +31,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -32,6 +39,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -47,22 +55,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -73,6 +96,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -91,9 +115,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -146,4 +172,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_unsaved_wizard.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_unsaved_wizard.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a8d685756ade6723d112ce7833b00d8eded810f7
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_unsaved_wizard.yml
@@ -0,0 +1,183 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_form_unsaved_wizard
+title: 'Test: Webform: Unsaved wizard'
+description: 'Test displaying unsaved data warning on wizard.'
+category: 'Test: Webform'
+elements: |
+  page_01:
+    '#type': webform_wizard_page
+    '#title': page_1
+    testfield_1:
+      '#type': textfield
+      '#title': textfield_1
+  page_02:
+    '#type': webform_wizard_page
+    '#title': page_2
+    testfield_1:
+      '#type': textfield
+      '#title': textfield_2
+css: ''
+javascript: ''
+settings:
+  ajax: true
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: true
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 1
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: true
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_validate.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_validate.yml
index aa3a7cbe7aea4d32576045cb85427ab3d3e9f258..45d8111329eaaf7e0f91afd7bc91790dc6a5542b 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_validate.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_validate.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_validate
 title: 'Test: Webform: Validate'
 description: 'Test executing $form["#validate"] handlers.'
@@ -24,6 +26,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -31,6 +34,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -46,22 +50,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -72,6 +91,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -90,9 +110,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -145,4 +167,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_access.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_access.yml
index 0341a00540c70138ea336ddee0151631c1e337d9..5379026fc271af857b60211aaefb672f5e0f255c 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_access.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_access.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_wizard_access
 title: 'Test: Webform: Wizard access'
 description: 'Test wizard with page specific access rules.'
@@ -62,6 +64,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -69,6 +72,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -84,22 +88,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -110,6 +129,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -128,9 +148,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -187,4 +209,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_advanced.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_advanced.yml
index b926c2710af3c512ba10ebd2a679a9a3e2189de1..523c6599314a7babe17103d797abeb4ba5c95edf 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_advanced.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_advanced.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_wizard_advanced
 title: 'Test: Webform: Wizard advanced'
 description: 'Test a multiple step ''wizard'' webform with save draft, auto save, and preview.'
@@ -66,6 +68,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -73,6 +76,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -88,22 +92,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: true
   wizard_progress_percentage: true
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -114,6 +133,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: authenticated
   draft_multiple: false
   draft_auto_save: true
@@ -132,9 +152,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -187,4 +209,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_basic.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_basic.yml
index 9f80133d13c0833ed21ead2fbbd6c91c4b260cb2..549c60e2615e8083b3aa09fd397f4874adbaa888 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_basic.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_basic.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_wizard_basic
 title: 'Test: Webform: Wizard basic'
 description: 'Test a basic multiple step ''wizard'' webform.'
@@ -35,6 +37,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -42,6 +45,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -57,22 +61,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -83,6 +102,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -101,9 +121,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -156,4 +178,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_conditional.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_conditional.yml
index 03bbc17eb83e1d6ae8df5ed6cb181f8aba0f49b4..6f932176fdc5144a99e9f35f43f73aa7caf2155d 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_conditional.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_conditional.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_wizard_conditional
 title: 'Test: Webform: Wizard conditional'
 description: 'Test a multiple step wizard with conditional pages'
@@ -16,7 +18,7 @@ elements: |
   start:
     '#title': Start
     '#type': webform_wizard_page
-    'trigger_pages':
+    trigger_pages:
       '#type': checkboxes
       '#title': trigger_pages
       '#options':
@@ -38,7 +40,7 @@ elements: |
         disabled:
           ':input[name="trigger_none"]':
             checked: true
-    'trigger_none':
+    trigger_none:
       '#type': checkbox
       '#title': trigger_none
   page_1:
@@ -50,7 +52,7 @@ elements: |
           checked: true
         ':input[name="trigger_none"]':
           unchecked: true
-    'page_1_markup':
+    page_1_markup:
       '#markup': 'This is page 1.'
   page_2:
     '#title': 'Page 2'
@@ -61,7 +63,7 @@ elements: |
           checked: true
         ':input[name="trigger_none"]':
           unchecked: true
-    'page_2_markup':
+    page_2_markup:
       '#markup': 'This is page 2.'
   page_3:
     '#title': 'Page 3'
@@ -72,7 +74,7 @@ elements: |
           checked: true
         ':input[name="trigger_none"]':
           unchecked: true
-    'page_3_markup':
+    page_3_markup:
       '#markup': 'This is page 3.'
   page_4:
     '#title': 'Page 4'
@@ -83,7 +85,7 @@ elements: |
           checked: true
         ':input[name="trigger_none"]':
           unchecked: true
-    'page_4_markup':
+    page_4_markup:
       '#markup': 'This is page 4.'
   page_5:
     '#title': 'Page 5'
@@ -94,7 +96,7 @@ elements: |
           checked: true
         ':input[name="trigger_none"]':
           unchecked: true
-    'page_5_markup':
+    page_5_markup:
       '#markup': 'This is page 5.'
 css: ''
 javascript: ''
@@ -104,6 +106,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -111,6 +114,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -126,22 +130,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -152,6 +171,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -170,9 +190,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -225,4 +247,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_custom.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_custom.yml
index 88cc26107d768838ab1be4f7d83fc6640b2bb258..b35a7dcf3b7684b80592d8a366e75899fe59954d 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_custom.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_custom.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_wizard_custom
 title: 'Test: Webform: Wizard custom'
 description: 'Test a multiple step ''wizard'' webform with custom hide/show pages logic.'
@@ -67,6 +69,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -74,6 +77,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: true
   form_prepopulate_source_entity: false
@@ -89,22 +93,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: true
   wizard_progress_percentage: true
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -115,6 +134,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -133,9 +153,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -188,4 +210,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_links.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_links.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5d6408c6a0cfbd5328ea806d49a73f2947fdd9e5
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_links.yml
@@ -0,0 +1,185 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_form_wizard_links
+title: 'Test: Webform: Wizard previous links'
+description: 'Test a multiple step ''wizard'' webform with previous page links.'
+category: 'Test: Webform'
+elements: |
+  page_1:
+    '#title': 'Page 1'
+    '#type': webform_wizard_page
+    element_1:
+      '#title': 'Element 1'
+      '#type': textfield
+      '#default_value': '{element_1}'
+  page_2:
+    '#title': 'Page 2'
+    '#type': webform_wizard_page
+    element_2:
+      '#title': 'Element 2'
+      '#type': textfield
+      '#default_value': '{element_2}'
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: true
+  wizard_start_label: ''
+  wizard_preview_link: true
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 1
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: all
+  draft_multiple: false
+  draft_auto_save: true
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_long_100.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_long_100.yml
index 477ffdbf31f8f850929b6ef577d5aece8dfe288f..f40b85c57989073363f4e010e9d5b5e074a644b9 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_long_100.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_long_100.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_wizard_long_100
 title: 'Test: Wizard: Long - 100 elements'
 description: 'Test wizard with 100 elements'
@@ -351,6 +353,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -358,6 +361,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -373,22 +377,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -399,6 +418,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -417,9 +437,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -472,4 +494,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_long_200.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_long_200.yml
index d0861cf9cbb23fb85180ad82d79152b911b32a10..f19678c472054fce25e50cdb7aec66a7930c2f80 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_long_200.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_long_200.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_wizard_long_200
 title: 'Test: Wizard: Long - 200 elements'
 description: 'Test wizard with 200 elements'
@@ -681,6 +683,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -688,6 +691,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -703,22 +707,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -729,6 +748,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -747,9 +767,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -802,4 +824,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_long_300.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_long_300.yml
index 41cc08c3a7b79936d1083d63ed4ac19b59a9b578..3757818a246d87e4fe0e8eee004633b88211c8c4 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_long_300.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_long_300.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_wizard_long_300
 title: 'Test: Wizard: Long - 300 elements'
 description: 'Test wizard with 300 elements'
@@ -1011,6 +1013,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -1018,6 +1021,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -1033,22 +1037,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -1059,6 +1078,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -1077,9 +1097,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -1132,4 +1154,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_validate.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_validate.yml
index 96723c33c6978b436569906f71a51dc9f6a2811a..5364771ccc2eb4dad4e8b90170cdb19fd7f5ed85 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_validate.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_validate.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_wizard_validate
 title: 'Test: Webform: Wizard element validation'
 description: 'Test a multiple step ''wizard'' webform element validation.'
@@ -64,6 +66,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -71,6 +74,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: true
   form_prepopulate_source_entity: false
@@ -86,22 +90,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: true
   wizard_progress_percentage: true
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -112,6 +131,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -130,9 +150,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -185,6 +207,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_validate_comp.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_validate_comp.yml
index 6e4adf504e17a5a74d81dc663e837b6269919d77..ce89a6f57bd9e77c7b83d03e07b59ff3fca6740f 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_validate_comp.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_wizard_validate_comp.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_form_wizard_validate_comp
 title: 'Test: Webform: Wizard composite element validation'
 description: 'Test a multiple step ''wizard'' webform composite element validation.'
@@ -112,6 +114,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -119,6 +122,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: true
   form_prepopulate_source_entity: false
@@ -134,22 +138,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: true
   wizard_progress_percentage: true
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -160,6 +179,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -178,9 +198,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -233,6 +255,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_action.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_action.yml
index 9c894d936f812f6de902fc22eaac2cdf04518d9d..1a19991c41c810d29935b5b4b38eda9084bf0f39 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_action.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_action.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_action
 title: 'Test: Handler: Action'
 description: 'Test action handler.'
@@ -53,6 +55,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -60,6 +63,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -75,22 +79,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: 'This is submission was automatically locked.'
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -101,6 +120,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: authenticated
   draft_multiple: false
   draft_auto_save: false
@@ -119,9 +139,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -174,6 +196,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   notes:
     id: action
@@ -196,6 +222,7 @@ handlers:
       data: |
         notes_add: ''
         notes_last: '[webform_submission:values:notes_add]'
+        
       message: 'Submission notes have been updated.'
       message_type: status
       debug: true
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email.yml
index fd9978da16871a9e21693fcc5d8cf74eefe72a4b..d2d87772a6e7560875ac08bd127fa0fe6e29f4ba 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_email
 title: 'Test: Handler: Email'
 description: 'Test base plain text email handler.'
@@ -46,6 +48,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -53,6 +56,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -68,22 +72,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -94,6 +113,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -112,9 +132,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -167,6 +189,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email:
     id: email
@@ -188,13 +214,15 @@ handlers:
       from_options: {  }
       from_name: '[webform_submission:values:first_name:raw] [webform_submission:values:last_name:raw]'
       subject: '[webform_submission:values:subject:raw]'
-      body: default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: false
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_advanced.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_advanced.yml
index b44348e50b1a9712da666fed37ffe8555e1e7df8..dae7577f749befc958fb9242a2fa7e218bc7a7b2 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_advanced.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_advanced.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_email_advanced
 title: 'Test: Handler: Email advanced'
 description: 'Test email HTML handler.'
@@ -41,6 +43,9 @@ elements: |
   optional:
     '#title': Optional
     '#type': textfield
+  checkbox:
+    '#title': Checkbox
+    '#type': checkbox
   file:
     '#type': managed_file
     '#title': File
@@ -58,6 +63,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -65,6 +71,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -80,22 +87,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -106,6 +128,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -124,9 +147,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -179,6 +204,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email:
     id: email
@@ -207,12 +236,15 @@ handlers:
         [webform_submission:values]
         <hr />
         <p style="color:yellow"><em>Custom styled HTML markup</em></p>
+        
       excluded_elements: {  }
       ignore_access: true
       exclude_empty: true
+      exclude_empty_checkbox: true
       html: true
       attachments: true
       twig: false
+      theme_name: ''
       debug: true
       reply_to: reply_to@example.com
       return_path: return_path@example.com
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_mapping.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_mapping.yml
index 139e16261207976a5a9f6f998ce4b2b66d27d410..64f6528ac3a6317c56bce00c106eaba4b0f16106 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_mapping.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_mapping.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_email_mapping
 title: 'Test: Handler: Email mapping'
 description: 'Test email mapping handler.'
@@ -36,6 +38,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -43,6 +46,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -58,22 +62,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -84,6 +103,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -102,9 +122,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -157,6 +179,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   select:
     id: email
@@ -176,17 +202,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: 'Email mapping handler: Select yes option'
-      body: default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -210,17 +238,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: 'Email mapping handler: Select empty'
-      body: default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -244,17 +274,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: 'Email mapping handler: Select default'
-      body: default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -280,17 +312,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: 'Email mapping handler: Checkboxes'
-      body: default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -314,17 +348,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: 'Email mapping handler: Radios other'
-      body: default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_roles.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_roles.yml
index ee27d8735e177228059ba453b8aff1fcbe06d725..cb9dcbb2a5f8ae56273f4e99e8d521841cbfd396 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_roles.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_roles.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_email_roles
 title: 'Test: Handler: Email roles'
 description: 'Test email roles.'
@@ -19,7 +21,7 @@ elements: |
     '#options':
       authenticated: 'Authenticated (authenticated)'
       administrator: 'Administrator (administrator)'
-      other: 'Other'
+      other: Other
 css: ''
 javascript: ''
 settings:
@@ -28,6 +30,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -35,6 +38,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -50,22 +54,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -76,6 +95,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -94,9 +114,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -149,6 +171,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email:
     id: email
@@ -172,17 +198,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_states.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_states.yml
index b00fe296cad930287505b72b2a80d3e1bec31def..1df0a5db40298ed2435a9e95f1570a1a6e94f0b3 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_states.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_states.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_email_states
 title: 'Test: Handler: Email states'
 description: 'Test sending email during each submission state.'
@@ -23,6 +25,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -30,6 +33,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: true
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -45,22 +49,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -71,6 +90,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: all
   draft_multiple: false
   draft_auto_save: false
@@ -89,9 +109,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -144,6 +166,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_draft:
     id: email
@@ -161,17 +187,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: 'Draft saved'
-      body: default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -193,17 +221,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: 'Submission converted'
-      body: default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -225,17 +255,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: 'Submission completed'
-      body: default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -257,17 +289,53 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: 'Submission updated'
-      body: default
+      body: _default
+      excluded_elements: {  }
+      ignore_access: false
+      exclude_empty: true
+      exclude_empty_checkbox: false
+      html: true
+      attachments: false
+      twig: false
+      theme_name: ''
+      debug: true
+      reply_to: ''
+      return_path: ''
+      sender_mail: ''
+      sender_name: ''
+  email_locked:
+    id: email
+    label: 'Submission locked'
+    handler_id: email_locked
+    status: true
+    conditions: {  }
+    weight: 3
+    settings:
+      states:
+        - locked
+      to_mail: locked@example.com
+      to_options: {  }
+      cc_mail: ''
+      cc_options: {  }
+      bcc_mail: ''
+      bcc_options: {  }
+      from_mail: _default
+      from_options: {  }
+      from_name: _default
+      subject: 'Submission locked'
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -289,17 +357,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: 'Submission deleted'
-      body: default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -320,17 +390,19 @@ handlers:
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: 'Submission custom'
-      body: default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_twig.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_twig.yml
index 9db8dbc11e9191aebe1711a221a5fc878add4bd3..5c867753aa6d4be1861bb8eefb6e37d664535837 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_twig.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_email_twig.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_email_twig
 title: 'Test: Handler: Email Twig'
 description: 'Test Twig email handler.'
@@ -46,6 +48,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -53,6 +56,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -68,22 +72,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -94,6 +113,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -112,9 +132,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -167,6 +189,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email:
     id: email
@@ -191,12 +217,15 @@ handlers:
       body: |
         <p>Submitted values are:</p>
         {{ webform_token('[webform_submission:values]', webform_submission) }}
+        
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: true
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_settings.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_settings.yml
index 9ea1cf8c17e2849d871893df8d6d31878d2bf69b..4087323aa1f27fb090c631cb3fd0256145561f99 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_settings.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_handler_settings.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_settings
 title: 'Test: Handler: Settings'
 description: 'Test settings handler.'
@@ -66,6 +68,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -73,6 +76,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -88,22 +92,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -114,6 +133,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: all
   draft_multiple: false
   draft_auto_save: false
@@ -132,9 +152,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -187,6 +209,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   preview:
     id: settings
@@ -204,7 +230,7 @@ handlers:
       confirmation_url: ''
       confirmation_title: ''
       confirmation_message: ''
-      debug: true
+      debug: '1'
   confirmation:
     id: settings
     label: 'Confirmation Settings'
@@ -221,7 +247,7 @@ handlers:
       confirmation_url: ''
       confirmation_title: '[webform_submission:values:confirmation_title]'
       confirmation_message: '[webform_submission:values:confirmation_message]'
-      debug: true
+      debug: '1'
   custom:
     id: settings
     label: 'Custom settings'
@@ -238,6 +264,6 @@ handlers:
       confirmation_url: ''
       confirmation_title: ''
       confirmation_message: ''
-      debug: true
+      debug: '1'
       draft_saved_message: '[webform_submission:values:draft_saved_message]'
       draft_loaded_message: '[webform_submission:values:draft_loaded_message]'
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_libraries_optional.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_libraries_optional.yml
index 5e7411cc535b4dc2404e150b777506dc7de692e8..91d43797709c1dab449c838c07f03e159a6b892e 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_libraries_optional.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_libraries_optional.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: 1
 template: false
+archive: false
 id: test_libraries_optional
 title: 'Test: Libraries: Optional'
 description: 'Test optional libraries.'
@@ -73,7 +75,7 @@ elements: |
           text: 'Cute Kitten 3'
           src: 'http://placekitten.com/130/200'
     location:
-      '#type': webform_location
+      '#type': webform_location_places
       '#title': Location
     rating:
       '#type': webform_rating
@@ -99,6 +101,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -106,6 +109,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -121,22 +125,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -147,6 +166,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -165,9 +185,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -220,4 +242,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_rendering.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_rendering.yml
index dfbc6cfb9c58bac90a9f53d9a57ccb3ed91f6258..4f22ff7a90972615c6b3b816ec961b075c1d7edf 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_rendering.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_rendering.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_rendering
 title: 'Test: Rendering'
 description: 'Test Webform and Webform Submission rendering'
@@ -55,6 +57,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -62,6 +65,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -77,22 +81,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: '[webform_submission:values:submission_label]'
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -103,6 +122,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -121,9 +141,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -176,6 +198,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_html:
     id: email
@@ -187,23 +213,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: '[webform_submission:values:submission_label:raw]'
       body: '[webform_submission:values]'
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
@@ -219,23 +247,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
+      from_name: _default
       subject: '[webform_submission:values:submission_label:raw]'
       body: '[webform_submission:values]'
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: false
       attachments: false
       twig: false
+      theme_name: ''
       debug: true
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states.yml
similarity index 93%
rename from web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states.yml
rename to web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states.yml
index d25e5a4637e9677063d506ed0759baadabc9ac0f..654e784690748cb08c0c0be2340a9df69866f01c 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states.yml
@@ -6,9 +6,11 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
-id: test_form_states
+archive: false
+id: test_states
 title: 'Test: Form API #states'
 description: 'Test Drupal''s #states API states (visible, required, disabled, checked, and expanded).'
 category: 'Test: Form API #states'
@@ -19,27 +21,6 @@ elements: |
     visible_trigger:
       '#type': checkbox
       '#title': 'Displays and require elements'
-    visible_processed_text:
-      '#type': processed_text
-      '#title': 'Advanced HTML/Text'
-      '#states':
-        visible:
-          ':input[name="visible_trigger"]':
-            checked: true
-        required:
-          ':input[name="visible_trigger"]':
-            checked: true
-    visible_webform_markup:
-      '#type': webform_markup
-      '#title': 'Basic HTML'
-      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/2700667">Issue #2700667: Notice: Undefined index: #type in drupal_process_states()</a>'
-      '#states':
-        visible:
-          ':input[name="visible_trigger"]':
-            checked: true
-        required:
-          ':input[name="visible_trigger"]':
-            checked: true
     visible_container:
       '#type': container
       '#title': Container
@@ -71,13 +52,9 @@ elements: |
         required:
           ':input[name="visible_trigger"]':
             checked: true
-    visible_item:
-      '#type': item
-      '#title': Item
-      '#markup': '{markup}'
-      '#field_prefix': '{field_prefix}'
-      '#field_suffix': '{field_suffix}'
-      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/783438">Issue #783438: #states doesn''t work for #type item</a>'
+    visible_webform_section:
+      '#type': webform_section
+      '#title': Section
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -85,9 +62,9 @@ elements: |
         required:
           ':input[name="visible_trigger"]':
             checked: true
-    visible_label:
-      '#type': label
-      '#title': Label
+    visible_table:
+      '#type': table
+      '#title': Table
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -95,12 +72,19 @@ elements: |
         required:
           ':input[name="visible_trigger"]':
             checked: true
-    visible_webform_message:
-      '#type': webform_message
-      '#title': Message
-      '#message_type': warning
-      '#message_message': 'This is a <strong>warning</strong> message.'
-      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/77245">Issue #77245: A place for JavaScript status messages</a>'
+    visible_address:
+      '#type': address
+      '#title': 'Advanced address'
+      '#default_value':
+        given_name: John
+        family_name: Smith
+        organization: 'Google Inc.'
+        address_line1: '1098 Alta Ave'
+        postal_code: '94043'
+        locality: 'Mountain View'
+        administrative_area: CA
+        country_code: US
+        langcode: en
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -108,9 +92,9 @@ elements: |
         required:
           ':input[name="visible_trigger"]':
             checked: true
-    visible_webform_section:
-      '#type': webform_section
-      '#title': Section
+    visible_processed_text:
+      '#type': processed_text
+      '#title': 'Advanced HTML/Text'
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -118,9 +102,20 @@ elements: |
         required:
           ':input[name="visible_trigger"]':
             checked: true
-    visible_table:
-      '#type': table
-      '#title': Table
+    visible_webform_audio_file:
+      '#type': webform_audio_file
+      '#title': 'Audio file'
+      '#states':
+        visible:
+          ':input[name="visible_trigger"]':
+            checked: true
+        required:
+          ':input[name="visible_trigger"]':
+            checked: true
+    visible_webform_autocomplete:
+      '#type': webform_autocomplete
+      '#title': Autocomplete
+      '#default_value': Loremipsum
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -130,13 +125,13 @@ elements: |
             checked: true
     visible_webform_address:
       '#type': webform_address
-      '#title': Address
+      '#title': 'Basic address'
       '#default_value':
         address: '10 Main Street'
         address_2: '10 Main Street'
         city: Springfield
         state_province: Alabama
-        postal_code: Loremipsum
+        postal_code: '11111'
         country: Afghanistan
       '#states':
         visible:
@@ -145,20 +140,10 @@ elements: |
         required:
           ':input[name="visible_trigger"]':
             checked: true
-    visible_webform_audio_file:
-      '#type': webform_audio_file
-      '#title': 'Audio file'
-      '#states':
-        visible:
-          ':input[name="visible_trigger"]':
-            checked: true
-        required:
-          ':input[name="visible_trigger"]':
-            checked: true
-    visible_webform_autocomplete:
-      '#type': webform_autocomplete
-      '#title': Autocomplete
-      '#default_value': Loremipsum
+    visible_webform_markup:
+      '#type': webform_markup
+      '#title': 'Basic HTML'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/2700667">Issue #2700667: Notice: Undefined index: #type in drupal_process_states()</a>'
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -281,40 +266,9 @@ elements: |
         required:
           ':input[name="visible_trigger"]':
             checked: true
-    visible_webform_composite:
-      '#type': webform_custom_composite
-      '#title': 'Composite custom'
-      '#element':
-        name:
-          '#type': textfield
-          '#title': Name
-          '#title_display': invisible
-        gender:
-          '#type': select
-          '#title': Gender
-          '#title_display': invisible
-          '#options':
-            Male: Male
-            Female: Female
-      '#default_value':
-        - name: Loremipsum
-          gender: Male
-        - name: Loremipsum
-          gender: Male
-        - name: Loremipsum
-          gender: Male
-      '#states':
-        visible:
-          ':input[name="visible_trigger"]':
-            checked: true
-        required:
-          ':input[name="visible_trigger"]':
-            checked: true
     visible_webform_computed_token:
       '#type': webform_computed_token
       '#title': 'Computed token'
-      '#value': 'This is a Computed token value.'
-      '#default_value': Loremipsum
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -322,11 +276,10 @@ elements: |
         required:
           ':input[name="visible_trigger"]':
             checked: true
+      '#template': 'This is a Computed token value.'
     visible_webform_computed_twig:
       '#type': webform_computed_twig
       '#title': 'Computed Twig'
-      '#value': 'This is a Computed Twig value.'
-      '#default_value': Loremipsum
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -334,6 +287,7 @@ elements: |
         required:
           ':input[name="visible_trigger"]':
             checked: true
+      '#template': 'This is a Computed Twig value.'
     visible_webform_contact:
       '#type': webform_contact
       '#title': Contact
@@ -346,7 +300,7 @@ elements: |
         address_2: '10 Main Street'
         city: Springfield
         state_province: Alabama
-        postal_code: Loremipsum
+        postal_code: '11111'
         country: Afghanistan
       '#states':
         visible:
@@ -355,10 +309,39 @@ elements: |
         required:
           ':input[name="visible_trigger"]':
             checked: true
+    visible_webform_custom_composite:
+      '#type': webform_custom_composite
+      '#title': 'Custom composite'
+      '#element':
+        name:
+          '#type': textfield
+          '#title': Name
+          '#title_display': invisible
+        gender:
+          '#type': select
+          '#title': Gender
+          '#title_display': invisible
+          '#options':
+            Male: Male
+            Female: Female
+      '#default_value':
+        - name: Loremipsum
+          gender: Male
+        - name: Loremipsum
+          gender: Male
+        - name: Loremipsum
+          gender: Male
+      '#states':
+        visible:
+          ':input[name="visible_trigger"]':
+            checked: true
+        required:
+          ':input[name="visible_trigger"]':
+            checked: true
     visible_date:
       '#type': date
       '#title': Date
-      '#default_value': '1942-06-18'
+      '#default_value': '2025-12-07T23:03:39-0500'
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -370,7 +353,7 @@ elements: |
       '#type': datetime
       '#title': Date/time
       '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/2419131">Issue #2419131: #states attribute does not work on #type datetime</a>'
-      '#default_value': '1942-06-18'
+      '#default_value': '2031-05-08T00:29:17-0400'
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -381,7 +364,7 @@ elements: |
     visible_datelist:
       '#type': datelist
       '#title': 'Date list'
-      '#default_value': '1942-06-18'
+      '#default_value': '2036-12-05T02:05:53-0500'
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -561,6 +544,30 @@ elements: |
         required:
           ':input[name="visible_trigger"]':
             checked: true
+    visible_item:
+      '#type': item
+      '#title': Item
+      '#markup': '{markup}'
+      '#field_prefix': '{field_prefix}'
+      '#field_suffix': '{field_suffix}'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/783438">Issue #783438: #states doesn''t work for #type item</a>'
+      '#states':
+        visible:
+          ':input[name="visible_trigger"]':
+            checked: true
+        required:
+          ':input[name="visible_trigger"]':
+            checked: true
+    visible_label:
+      '#type': label
+      '#title': Label
+      '#states':
+        visible:
+          ':input[name="visible_trigger"]':
+            checked: true
+        required:
+          ':input[name="visible_trigger"]':
+            checked: true
     visible_language_select:
       '#type': language_select
       '#title': 'Language select'
@@ -607,8 +614,8 @@ elements: |
         required:
           ':input[name="visible_trigger"]':
             checked: true
-    visible_webform_location:
-      '#type': webform_location
+    visible_webform_location_places:
+      '#type': webform_location_places
       '#title': Location
       '#map': true
       '#geolocation': true
@@ -655,6 +662,19 @@ elements: |
         required:
           ':input[name="visible_trigger"]':
             checked: true
+    visible_webform_message:
+      '#type': webform_message
+      '#title': Message
+      '#message_type': warning
+      '#message_message': 'This is a <strong>warning</strong> message.'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/77245">Issue #77245: A place for JavaScript status messages</a>'
+      '#states':
+        visible:
+          ':input[name="visible_trigger"]':
+            checked: true
+        required:
+          ':input[name="visible_trigger"]':
+            checked: true
     visible_webform_name:
       '#type': webform_name
       '#title': Name
@@ -747,7 +767,7 @@ elements: |
       '#min': 0
       '#max': 100
       '#step': 1
-      '#output': right
+      '#output': below
       '#output__field_prefix': $
       '#output__field_suffix': '.00'
       '#states':
@@ -760,7 +780,6 @@ elements: |
     visible_webform_rating:
       '#type': webform_rating
       '#title': Rating
-      '#default_value': 1
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -905,9 +924,9 @@ elements: |
       '#title': 'Term checkboxes'
       '#vocabulary': tags
       '#default_value':
-        - 41
-        - 48
-        - 43
+        - Loremipsum
+        - Oratione
+        - Dixisset
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -919,7 +938,7 @@ elements: |
       '#type': webform_term_select
       '#title': 'Term select'
       '#vocabulary': tags
-      '#default_value': 41
+      '#default_value': Loremipsum
       '#states':
         visible:
           ':input[name="visible_trigger"]':
@@ -1045,21 +1064,6 @@ elements: |
     invisible_trigger:
       '#type': checkbox
       '#title': 'Hide and empty elements'
-    invisible_processed_text:
-      '#type': processed_text
-      '#title': 'Advanced HTML/Text'
-      '#states':
-        invisible:
-          ':input[name="invisible_trigger"]':
-            checked: true
-    invisible_webform_markup:
-      '#type': webform_markup
-      '#title': 'Basic HTML'
-      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/2700667">Issue #2700667: Notice: Undefined index: #type in drupal_process_states()</a>'
-      '#states':
-        invisible:
-          ':input[name="invisible_trigger"]':
-            checked: true
     invisible_container:
       '#type': container
       '#title': Container
@@ -1082,73 +1086,77 @@ elements: |
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
-    invisible_item:
-      '#type': item
-      '#title': Item
-      '#markup': '{markup}'
-      '#field_prefix': '{field_prefix}'
-      '#field_suffix': '{field_suffix}'
-      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/783438">Issue #783438: #states doesn''t work for #type item</a>'
+    invisible_webform_section:
+      '#type': webform_section
+      '#title': Section
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
-    invisible_label:
-      '#type': label
-      '#title': Label
+    invisible_table:
+      '#type': table
+      '#title': Table
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
-    invisible_webform_message:
-      '#type': webform_message
-      '#title': Message
-      '#message_type': warning
-      '#message_message': 'This is a <strong>warning</strong> message.'
-      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/77245">Issue #77245: A place for JavaScript status messages</a>'
+    invisible_address:
+      '#type': address
+      '#title': 'Advanced address'
+      '#default_value':
+        given_name: John
+        family_name: Smith
+        organization: 'Google Inc.'
+        address_line1: '1098 Alta Ave'
+        postal_code: '94043'
+        locality: 'Mountain View'
+        administrative_area: CA
+        country_code: US
+        langcode: en
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
-    invisible_webform_section:
-      '#type': webform_section
-      '#title': Section
+    invisible_processed_text:
+      '#type': processed_text
+      '#title': 'Advanced HTML/Text'
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
-    invisible_table:
-      '#type': table
-      '#title': Table
+    invisible_webform_audio_file:
+      '#type': webform_audio_file
+      '#title': 'Audio file'
+      '#states':
+        invisible:
+          ':input[name="invisible_trigger"]':
+            checked: true
+    invisible_webform_autocomplete:
+      '#type': webform_autocomplete
+      '#title': Autocomplete
+      '#default_value': Loremipsum
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
     invisible_webform_address:
       '#type': webform_address
-      '#title': Address
+      '#title': 'Basic address'
       '#default_value':
         address: '10 Main Street'
         address_2: '10 Main Street'
         city: Springfield
         state_province: Alabama
-        postal_code: Loremipsum
+        postal_code: '11111'
         country: Afghanistan
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
-    invisible_webform_audio_file:
-      '#type': webform_audio_file
-      '#title': 'Audio file'
-      '#states':
-        invisible:
-          ':input[name="invisible_trigger"]':
-            checked: true
-    invisible_webform_autocomplete:
-      '#type': webform_autocomplete
-      '#title': Autocomplete
-      '#default_value': Loremipsum
+    invisible_webform_markup:
+      '#type': webform_markup
+      '#title': 'Basic HTML'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/2700667">Issue #2700667: Notice: Undefined index: #type in drupal_process_states()</a>'
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
@@ -1244,50 +1252,22 @@ elements: |
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
-    invisible_webform_composite:
-      '#type': webform_custom_composite
-      '#title': 'Composite custom'
-      '#element':
-        name:
-          '#type': textfield
-          '#title': Name
-          '#title_display': invisible
-        gender:
-          '#type': select
-          '#title': Gender
-          '#title_display': invisible
-          '#options':
-            Male: Male
-            Female: Female
-      '#default_value':
-        - name: Loremipsum
-          gender: Male
-        - name: Loremipsum
-          gender: Male
-        - name: Loremipsum
-          gender: Male
-      '#states':
-        invisible:
-          ':input[name="invisible_trigger"]':
-            checked: true
     invisible_webform_computed_token:
       '#type': webform_computed_token
       '#title': 'Computed token'
-      '#value': 'This is a Computed token value.'
-      '#default_value': Loremipsum
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
+      '#template': 'This is a Computed token value.'
     invisible_webform_computed_twig:
       '#type': webform_computed_twig
       '#title': 'Computed Twig'
-      '#value': 'This is a Computed Twig value.'
-      '#default_value': Loremipsum
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
+      '#template': 'This is a Computed Twig value.'
     invisible_webform_contact:
       '#type': webform_contact
       '#title': Contact
@@ -1300,16 +1280,42 @@ elements: |
         address_2: '10 Main Street'
         city: Springfield
         state_province: Alabama
-        postal_code: Loremipsum
+        postal_code: '11111'
         country: Afghanistan
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
+    invisible_webform_custom_composite:
+      '#type': webform_custom_composite
+      '#title': 'Custom composite'
+      '#element':
+        name:
+          '#type': textfield
+          '#title': Name
+          '#title_display': invisible
+        gender:
+          '#type': select
+          '#title': Gender
+          '#title_display': invisible
+          '#options':
+            Male: Male
+            Female: Female
+      '#default_value':
+        - name: Loremipsum
+          gender: Male
+        - name: Loremipsum
+          gender: Male
+        - name: Loremipsum
+          gender: Male
+      '#states':
+        invisible:
+          ':input[name="invisible_trigger"]':
+            checked: true
     invisible_date:
       '#type': date
       '#title': Date
-      '#default_value': '1942-06-18'
+      '#default_value': '2029-09-22T22:31:32-0400'
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
@@ -1318,7 +1324,7 @@ elements: |
       '#type': datetime
       '#title': Date/time
       '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/2419131">Issue #2419131: #states attribute does not work on #type datetime</a>'
-      '#default_value': '1942-06-18'
+      '#default_value': '2012-11-08T22:22:22-0500'
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
@@ -1326,7 +1332,7 @@ elements: |
     invisible_datelist:
       '#type': datelist
       '#title': 'Date list'
-      '#default_value': '1942-06-18'
+      '#default_value': '2030-03-15T14:17:44-0400'
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
@@ -1467,6 +1473,24 @@ elements: |
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
+    invisible_item:
+      '#type': item
+      '#title': Item
+      '#markup': '{markup}'
+      '#field_prefix': '{field_prefix}'
+      '#field_suffix': '{field_suffix}'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/783438">Issue #783438: #states doesn''t work for #type item</a>'
+      '#states':
+        invisible:
+          ':input[name="invisible_trigger"]':
+            checked: true
+    invisible_label:
+      '#type': label
+      '#title': Label
+      '#states':
+        invisible:
+          ':input[name="invisible_trigger"]':
+            checked: true
     invisible_language_select:
       '#type': language_select
       '#title': 'Language select'
@@ -1504,8 +1528,8 @@ elements: |
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
-    invisible_webform_location:
-      '#type': webform_location
+    invisible_webform_location_places:
+      '#type': webform_location_places
       '#title': Location
       '#map': true
       '#geolocation': true
@@ -1543,6 +1567,16 @@ elements: |
         invisible:
           ':input[name="invisible_trigger"]':
             checked: true
+    invisible_webform_message:
+      '#type': webform_message
+      '#title': Message
+      '#message_type': warning
+      '#message_message': 'This is a <strong>warning</strong> message.'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/77245">Issue #77245: A place for JavaScript status messages</a>'
+      '#states':
+        invisible:
+          ':input[name="invisible_trigger"]':
+            checked: true
     invisible_webform_name:
       '#type': webform_name
       '#title': Name
@@ -1617,7 +1651,7 @@ elements: |
       '#min': 0
       '#max': 100
       '#step': 1
-      '#output': right
+      '#output': below
       '#output__field_prefix': $
       '#output__field_suffix': '.00'
       '#states':
@@ -1627,7 +1661,6 @@ elements: |
     invisible_webform_rating:
       '#type': webform_rating
       '#title': Rating
-      '#default_value': 1
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
@@ -1742,9 +1775,9 @@ elements: |
       '#title': 'Term checkboxes'
       '#vocabulary': tags
       '#default_value':
-        - 41
-        - 48
-        - 43
+        - Loremipsum
+        - Oratione
+        - Dixisset
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
@@ -1753,7 +1786,7 @@ elements: |
       '#type': webform_term_select
       '#title': 'Term select'
       '#vocabulary': tags
-      '#default_value': 41
+      '#default_value': Loremipsum
       '#states':
         invisible:
           ':input[name="invisible_trigger"]':
@@ -1849,16 +1882,26 @@ elements: |
     disabled_trigger:
       '#type': checkbox
       '#title': 'Disable elements'
-    disabled_webform_address:
-      '#type': webform_address
-      '#title': Address
+    disabled_address:
+      '#type': address
+      '#title': 'Advanced address'
       '#default_value':
-        address: '10 Main Street'
-        address_2: '10 Main Street'
-        city: Springfield
-        state_province: Alabama
-        postal_code: Loremipsum
-        country: Afghanistan
+        given_name: John
+        family_name: Smith
+        organization: 'Google Inc.'
+        address_line1: '1098 Alta Ave'
+        postal_code: '94043'
+        locality: 'Mountain View'
+        administrative_area: CA
+        country_code: US
+        langcode: en
+      '#states':
+        disabled:
+          ':input[name="disabled_trigger"]':
+            checked: true
+    disabled_processed_text:
+      '#type': processed_text
+      '#title': 'Advanced HTML/Text'
       '#states':
         disabled:
           ':input[name="disabled_trigger"]':
@@ -1878,6 +1921,28 @@ elements: |
         disabled:
           ':input[name="disabled_trigger"]':
             checked: true
+    disabled_webform_address:
+      '#type': webform_address
+      '#title': 'Basic address'
+      '#default_value':
+        address: '10 Main Street'
+        address_2: '10 Main Street'
+        city: Springfield
+        state_province: Alabama
+        postal_code: '11111'
+        country: Afghanistan
+      '#states':
+        disabled:
+          ':input[name="disabled_trigger"]':
+            checked: true
+    disabled_webform_markup:
+      '#type': webform_markup
+      '#title': 'Basic HTML'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/2700667">Issue #2700667: Notice: Undefined index: #type in drupal_process_states()</a>'
+      '#states':
+        disabled:
+          ':input[name="disabled_trigger"]':
+            checked: true
     disabled_webform_buttons:
       '#type': webform_buttons
       '#title': Buttons
@@ -1969,50 +2034,22 @@ elements: |
         disabled:
           ':input[name="disabled_trigger"]':
             checked: true
-    disabled_webform_composite:
-      '#type': webform_custom_composite
-      '#title': 'Composite custom'
-      '#element':
-        name:
-          '#type': textfield
-          '#title': Name
-          '#title_display': invisible
-        gender:
-          '#type': select
-          '#title': Gender
-          '#title_display': invisible
-          '#options':
-            Male: Male
-            Female: Female
-      '#default_value':
-        - name: Loremipsum
-          gender: Male
-        - name: Loremipsum
-          gender: Male
-        - name: Loremipsum
-          gender: Male
-      '#states':
-        disabled:
-          ':input[name="disabled_trigger"]':
-            checked: true
     disabled_webform_computed_token:
       '#type': webform_computed_token
       '#title': 'Computed token'
-      '#value': 'This is a Computed token value.'
-      '#default_value': Loremipsum
       '#states':
         disabled:
           ':input[name="disabled_trigger"]':
             checked: true
+      '#template': 'This is a Computed token value.'
     disabled_webform_computed_twig:
       '#type': webform_computed_twig
       '#title': 'Computed Twig'
-      '#value': 'This is a Computed Twig value.'
-      '#default_value': Loremipsum
       '#states':
         disabled:
           ':input[name="disabled_trigger"]':
             checked: true
+      '#template': 'This is a Computed Twig value.'
     disabled_webform_contact:
       '#type': webform_contact
       '#title': Contact
@@ -2025,16 +2062,42 @@ elements: |
         address_2: '10 Main Street'
         city: Springfield
         state_province: Alabama
-        postal_code: Loremipsum
+        postal_code: '11111'
         country: Afghanistan
       '#states':
         disabled:
           ':input[name="disabled_trigger"]':
             checked: true
+    disabled_webform_custom_composite:
+      '#type': webform_custom_composite
+      '#title': 'Custom composite'
+      '#element':
+        name:
+          '#type': textfield
+          '#title': Name
+          '#title_display': invisible
+        gender:
+          '#type': select
+          '#title': Gender
+          '#title_display': invisible
+          '#options':
+            Male: Male
+            Female: Female
+      '#default_value':
+        - name: Loremipsum
+          gender: Male
+        - name: Loremipsum
+          gender: Male
+        - name: Loremipsum
+          gender: Male
+      '#states':
+        disabled:
+          ':input[name="disabled_trigger"]':
+            checked: true
     disabled_date:
       '#type': date
       '#title': Date
-      '#default_value': '1942-06-18'
+      '#default_value': '2015-04-28T17:21:41-0400'
       '#states':
         disabled:
           ':input[name="disabled_trigger"]':
@@ -2043,7 +2106,7 @@ elements: |
       '#type': datetime
       '#title': Date/time
       '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/2419131">Issue #2419131: #states attribute does not work on #type datetime</a>'
-      '#default_value': '1942-06-18'
+      '#default_value': '2009-06-05T06:32:08-0400'
       '#states':
         disabled:
           ':input[name="disabled_trigger"]':
@@ -2051,7 +2114,7 @@ elements: |
     disabled_datelist:
       '#type': datelist
       '#title': 'Date list'
-      '#default_value': '1942-06-18'
+      '#default_value': '2012-04-05T08:52:12-0400'
       '#states':
         disabled:
           ':input[name="disabled_trigger"]':
@@ -2192,6 +2255,24 @@ elements: |
         disabled:
           ':input[name="disabled_trigger"]':
             checked: true
+    disabled_item:
+      '#type': item
+      '#title': Item
+      '#markup': '{markup}'
+      '#field_prefix': '{field_prefix}'
+      '#field_suffix': '{field_suffix}'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/783438">Issue #783438: #states doesn''t work for #type item</a>'
+      '#states':
+        disabled:
+          ':input[name="disabled_trigger"]':
+            checked: true
+    disabled_label:
+      '#type': label
+      '#title': Label
+      '#states':
+        disabled:
+          ':input[name="disabled_trigger"]':
+            checked: true
     disabled_language_select:
       '#type': language_select
       '#title': 'Language select'
@@ -2229,8 +2310,8 @@ elements: |
         disabled:
           ':input[name="disabled_trigger"]':
             checked: true
-    disabled_webform_location:
-      '#type': webform_location
+    disabled_webform_location_places:
+      '#type': webform_location_places
       '#title': Location
       '#map': true
       '#geolocation': true
@@ -2268,6 +2349,16 @@ elements: |
         disabled:
           ':input[name="disabled_trigger"]':
             checked: true
+    disabled_webform_message:
+      '#type': webform_message
+      '#title': Message
+      '#message_type': warning
+      '#message_message': 'This is a <strong>warning</strong> message.'
+      '#description': '<b>Known Issues:</b><br /><a href="https://www.drupal.org/node/77245">Issue #77245: A place for JavaScript status messages</a>'
+      '#states':
+        disabled:
+          ':input[name="disabled_trigger"]':
+            checked: true
     disabled_webform_name:
       '#type': webform_name
       '#title': Name
@@ -2342,7 +2433,7 @@ elements: |
       '#min': 0
       '#max': 100
       '#step': 1
-      '#output': right
+      '#output': below
       '#output__field_prefix': $
       '#output__field_suffix': '.00'
       '#states':
@@ -2352,7 +2443,6 @@ elements: |
     disabled_webform_rating:
       '#type': webform_rating
       '#title': Rating
-      '#default_value': 1
       '#states':
         disabled:
           ':input[name="disabled_trigger"]':
@@ -2467,9 +2557,9 @@ elements: |
       '#title': 'Term checkboxes'
       '#vocabulary': tags
       '#default_value':
-        - 41
-        - 48
-        - 43
+        - Loremipsum
+        - Oratione
+        - Dixisset
       '#states':
         disabled:
           ':input[name="disabled_trigger"]':
@@ -2478,7 +2568,7 @@ elements: |
       '#type': webform_term_select
       '#title': 'Term select'
       '#vocabulary': tags
-      '#default_value': 41
+      '#default_value': Loremipsum
       '#states':
         disabled:
           ':input[name="disabled_trigger"]':
@@ -2576,6 +2666,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -2583,6 +2674,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -2598,22 +2690,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -2624,6 +2731,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -2642,9 +2750,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -2697,4 +2807,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_autocomplete.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_autocomplete.yml
new file mode 100644
index 0000000000000000000000000000000000000000..193079f38b5e36527a530d9d95b033cd3d7b9c5a
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_autocomplete.yml
@@ -0,0 +1,315 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_states_autocomplete
+title: 'Test: Form API #states autocomplete'
+description: 'Test Drupal''s #states autocomplete.'
+category: 'Test: Form API #states'
+elements: |
+  select:
+    '#type': select
+    '#title': select
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+  select_multiple:
+    '#type': select
+    '#title': select_multiple
+    '#multiple': true
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+  webform_select_other:
+    '#type': webform_select_other
+    '#title': webform_select_other
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+  radios:
+    '#type': radios
+    '#title': radios
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+  webform_radios_other:
+    '#type': webform_radios_other
+    '#title': webform_radios_other
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+  checkboxes:
+    '#type': checkboxes
+    '#title': checkboxes
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+  webform_checkboxes_other:
+    '#type': webform_checkboxes_other
+    '#title': webform_checkboxes_other
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+  webform_buttons:
+    '#type': webform_buttons
+    '#title': webform_buttons
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+  webform_buttons_other:
+    '#type': webform_buttons_other
+    '#title': webform_buttons_other
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+  tableselect_single:
+    '#type': tableselect
+    '#title': tableselect_single
+    '#multiple': false
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+  tableselect_multiple:
+    '#type': tableselect
+    '#title': tableselect_multiple
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+  webform_entity_select:
+    '#type': webform_entity_select
+    '#title': webform_entity_select
+    '#target_type': user
+    '#selection_handler': 'default:user'
+    '#selection_settings':
+      include_anonymous: true
+  webform_likert:
+    '#type': webform_likert
+    '#title': webform_likert
+    '#questions':
+      q1: 'Question 1'
+      q2: 'Question 2'
+      q3: 'Question 3'
+    '#answers':
+      1: 'Option 1'
+      2: 'Option 2'
+      3: 'Option 3'
+  webform_address:
+    '#type': webform_address
+    '#title': webform_address
+    '#state_province__type': webform_select_other
+  webform_custom_composite:
+    '#type': webform_custom_composite
+    '#title': webform_custom_composite
+    '#element':
+      textfield:
+        '#type': textfield
+        '#title': textfield
+      select:
+        '#type': select
+        '#title': select
+        '#options':
+          One: One
+          Two: Two
+          Three: Three
+  webform_element_composite:
+    '#type': webform_element_composite
+    '#title': webform_element_composite
+    '#default_value':
+      first_name:
+        '#type': textfield
+        '#title': 'First name'
+      last_name:
+        '#type': textfield
+        '#title': 'Last name'
+      gender:
+        '#type': select
+        '#options':
+          Male: Male
+          Female: Female
+        '#title': Gender
+  webform_image_select:
+    '#type': webform_image_select
+    '#title': webform_image_select
+    '#images':
+      kitten_1:
+        text: 'Cute Kitten 1'
+        src: 'http://placekitten.com/220/200'
+      kitten_2:
+        text: 'Cute Kitten 2'
+        src: 'http://placekitten.com/180/200'
+      kitten_3:
+        text: 'Cute Kitten 3'
+        src: 'http://placekitten.com/130/200'
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: true
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: true
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_crosspage.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_crosspage.yml
new file mode 100644
index 0000000000000000000000000000000000000000..04e2c52054498338bef9f95e0d31e4b92addef47
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_crosspage.yml
@@ -0,0 +1,201 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_states_crosspage
+title: 'Test: Form API #states cross page conditions'
+description: 'Test Drupal''s #states cross page conditions.'
+category: 'Test: Form API #states'
+elements: |
+  page_1:
+    '#type': webform_wizard_page
+    '#title': page_1
+    trigger_1:
+      '#type': checkbox
+      '#title': trigger_1
+    dependent_1:
+      '#type': textfield
+      '#title': dependent_1
+      '#states':
+        visible:
+          ':input[name="trigger_1"]':
+            checked: true
+          ':input[name="trigger_2"]':
+            checked: true
+  page_2:
+    '#type': webform_wizard_page
+    '#title': page_2
+    trigger_2:
+      '#type': checkbox
+      '#title': trigger_2
+    dependent_2:
+      '#type': textfield
+      '#title': dependent_2
+      '#states':
+        visible:
+          ':input[name="trigger_1"]':
+            checked: true
+          ':input[name="trigger_2"]':
+            checked: true
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: true
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: true
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_clear.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_clear.yml
new file mode 100644
index 0000000000000000000000000000000000000000..eed6d97e3de35991bd1ca29e8488ef96fb1b45d8
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_clear.yml
@@ -0,0 +1,266 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_states_server_clear
+title: 'Test: Form API #states save hide/show element without clearing default value'
+description: 'Test Drupal&#39;s #states API hide/show element without clearing default value.'
+category: 'Test: Form API #states'
+elements: |
+  trigger_checkbox:
+    '#type': checkbox
+    '#title': trigger_checkbox
+  dependent_hidden:
+    '#type': hidden
+    '#title': dependent_hidden
+    '#default_value': '{dependent_hidden}'
+    '#states_clear': false
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+  dependent_checkbox:
+    '#type': checkbox
+    '#title': dependent_checkbox
+    '#default_value': true
+    '#states_clear': false
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+  dependent_radios:
+    '#type': radios
+    '#title': dependent_radios
+    '#default_value': One
+    '#options':
+      One: One
+      Two: Two
+      Three: Three
+    '#states_clear': false
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+  dependent_value:
+    '#type': value
+    '#title': dependent_value
+    '#value': '{value}'
+    '#states_clear': false
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+  dependent_textfield:
+    '#type': textfield
+    '#title': dependent_textfield
+    '#default_value': '{dependent_textfield}'
+    '#states_clear': false
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+  dependent_textfield_multiple:
+    '#type': textfield
+    '#title': dependent_textfield
+    '#multiple': true
+    '#default_value':
+      - '{dependent_textfield}'
+    '#states_clear': false
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+  dependent_webform_name:
+    '#type': webform_name
+    '#title': webform_name
+    '#multiple': true
+    '#default_value':
+      - first: John
+        last: Smith
+    '#states_clear': false
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+  dependent_details:
+    '#type': details
+    '#title': dependent_details
+    '#states_clear': false
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+    dependent_details_textfield:
+      '#type': textfield
+      '#title': dependent_details_textfield
+      '#default_value': '{dependent_details_textfield}'
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: true
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 0
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_comp.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_comp.yml
similarity index 74%
rename from web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_comp.yml
rename to web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_comp.yml
index 5e6d5b681c341b9d644dcd8ef685246030a01b7d..11fd1f842a98e09e5b5cfb2200b096c010b74b09 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_comp.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_comp.yml
@@ -6,9 +6,11 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
-id: test_form_states_server_comp
+archive: false
+id: test_states_server_comp
 title: 'Test: Form API #states server-side composite elements'
 description: 'Test Drupal''s #states API for composite elements via server-side.'
 category: 'Test: Form API #states'
@@ -56,6 +58,24 @@ elements: |
       visible:
         ':input[name="webform_name_multiple_header_trigger"]':
           checked: true
+  webform_name_nested_trigger:
+    '#type': checkbox
+    '#title': webform_name_nested_trigger
+    '#default_value': false
+  webform_name_nested_trigger_fieldset:
+    '#type': fieldset
+    '#title': webform_name_nested_fieldset
+    '#states':
+      visible:
+        ':input[name="webform_name_nested_trigger"]':
+          checked: true
+    webform_name_nested:
+      '#type': webform_name
+      '#title': webform_name_nested
+      '#first__required': true
+      '#first__title': webform_name_nested_first
+      '#last__required': true
+      '#last__title': webform_name_nested_last
 css: ''
 javascript: ''
 settings:
@@ -64,6 +84,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -71,6 +92,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -86,22 +108,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -112,6 +149,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -130,9 +168,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -185,4 +225,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_containers.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_containers.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3d8c17a29797a6dd7a7cbdf312f0207ff4e7b060
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_containers.yml
@@ -0,0 +1,229 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_states_server_containers
+title: 'Test: Form API #states nested elements in a conditional hidden container'
+description: 'Test Drupal''s #states for nested elements in a conditional hidden container.'
+category: 'Test: Form API #states'
+elements: |
+  visible_trigger:
+    '#type': checkbox
+    '#title': visible_trigger
+  visible_fieldset:
+    '#type': fieldset
+    '#title': visible_fieldset
+    '#states':
+      visible:
+        ':input[name="visible_trigger"]':
+          checked: true
+    visible_textfield:
+      '#type': textfield
+      '#title': visible_textfield
+      '#required': true
+    visible_composite:
+      '#type': webform_custom_composite
+      '#title': visible_composite
+      '#element':
+        textfield:
+          '#type': textfield
+          '#title': textfield
+          '#required': true
+        select_other:
+          '#type': webform_select_other
+          '#title': select_other
+          '#options':
+            one: One
+            two: Two
+          '#required': true
+    visible_custom_textfield:
+      '#type': textfield
+      '#title': visible_custom_textfield
+      '#states':
+        required:
+          ':input[name="visible_trigger"]':
+            checked: true
+          ':input[name="visible_textfield"]':
+            filled: true
+  visible_slide_fieldset:
+    '#type': fieldset
+    '#title': visible_slide_fieldset
+    '#states':
+      visible-slide:
+        ':input[name="visible_trigger"]':
+          checked: true
+    visible_slide_textfield:
+      '#type': textfield
+      '#title': visible_slide_textfield
+      '#required': true
+    visible_slide_custom_textfield:
+      '#type': textfield
+      '#title': visible_slide_custom_textfield
+      '#states':
+        required:
+          ':input[name="visible_trigger"]':
+            checked: true
+          ':input[name="visible_slide_textfield"]':
+            filled: true
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: true
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: true
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_custom.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_custom.yml
similarity index 83%
rename from web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_custom.yml
rename to web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_custom.yml
index 30a8db412e6c1eb9caab305f4e1ed09ec733663e..c3ea741a0d4ae98ef638bda77e2d5ef78251a267 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_custom.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_custom.yml
@@ -6,9 +6,11 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
-id: test_form_states_server_custom
+archive: false
+id: test_states_server_custom
 title: 'Test: Form API #states custom pattern, less, and greater condition validation'
 description: 'Test Drupal''s #states API custom pattern, less, and greater condition validation.'
 category: 'Test: Form API #states'
@@ -40,11 +42,11 @@ elements: |
       visible:
         ':input[name="trigger_not_pattern"]':
           value:
-            '!pattern': '^$'
+            '!pattern': ^$
       required:
         ':input[name="trigger_not_pattern"]':
           value:
-            '!pattern': '^$'
+            '!pattern': ^$
   trigger_less:
     '#type': number
     '#title': trigger_less
@@ -95,6 +97,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -102,6 +105,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -117,22 +121,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -143,6 +162,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -161,9 +181,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -216,4 +238,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_multiple.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_multiple.yml
similarity index 78%
rename from web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_multiple.yml
rename to web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_multiple.yml
index fc188b16f256c5b73122bb792e279a900ae13199..0e4bcc3caab795b5d282df8bf77f5e2b3b50fea7 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_multiple.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_multiple.yml
@@ -6,16 +6,18 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
-id: test_form_states_server_multiple
+archive: false
+id: test_states_server_multiple
 title: 'Test: Form API #states server-side multiple elements'
 description: 'Test Drupal''s #states API for multiple elements via server-side.'
 category: 'Test: Form API #states'
 elements: |
   trigger_required:
     '#type': checkbox
-    '#title':  trigger_required
+    '#title': trigger_required
     '#default_value': false
   textfield_multiple:
     '#type': textfield
@@ -33,6 +35,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -40,6 +43,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -55,22 +59,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -81,6 +100,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -99,9 +119,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -154,4 +176,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_nested.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_nested.yml
new file mode 100644
index 0000000000000000000000000000000000000000..212b383060c64b4a185120a1944f307e7c11d787
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_nested.yml
@@ -0,0 +1,214 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_states_server_nested
+title: 'Test: Form API #states nested conditions'
+description: 'Test Drupal''s #states nested conditions support.'
+category: 'Test: Form API #states'
+elements: |
+  page_1:
+    '#type': webform_wizard_page
+    '#title': page_1
+    a:
+      '#type': checkbox
+      '#title': a
+      '#default_value': true
+    b:
+      '#type': checkbox
+      '#title': b
+      '#default_value': true
+    c:
+      '#type': checkbox
+      '#title': c
+    page_1_target:
+      '#type': textfield
+      '#title': 'page_1_target: [a and b] or c = required'
+      '#states':
+        required:
+          - ':input[name="c"]':
+              checked: true
+          - or
+          -
+            - ':input[name="a"]':
+                checked: true
+              ':input[name="b"]':
+                checked: true
+  page_2:
+    '#type': webform_wizard_page
+    '#title': page_2
+    page_2_target:
+      '#type': textfield
+      '#title': 'page_2_target: [a and b] or c = required'
+      '#states':
+        required:
+          - ':input[name="c"]':
+              checked: true
+          - or
+          -
+            - ':input[name="a"]':
+                checked: true
+              ':input[name="b"]':
+                checked: true
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: true
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: true
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_preview.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_preview.yml
similarity index 81%
rename from web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_preview.yml
rename to web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_preview.yml
index 59fae3a4fe3060a9e2c065325cfed4b0d856f43e..1d9a651ee78eadc2b761166fc468a455c9ebaaca 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_preview.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_preview.yml
@@ -6,9 +6,11 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
-id: test_form_states_server_preview
+archive: false
+id: test_states_server_preview
 title: 'Test: Form API #states preview hide/show condition validation'
 description: 'Test Drupal''s #states API hide/show condition validation.'
 category: 'Test: Form API #states'
@@ -58,6 +60,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -65,6 +68,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -80,22 +84,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -106,6 +125,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -124,9 +144,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -179,4 +201,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_required.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_required.yml
similarity index 90%
rename from web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_required.yml
rename to web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_required.yml
index ba2cb3d5b8e8561bdf137bc7ed4ffe23c3965b76..b4d3f59494306b82fc78714aaec3925c6c0ca18f 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_required.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_required.yml
@@ -6,9 +6,11 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
-id: test_form_states_server_required
+archive: false
+id: test_states_server_required
 title: 'Test: Form API #states server-side required validation'
 description: 'Test Drupal''s #states API via server-side required validation.'
 category: 'Test: Form API #states'
@@ -97,7 +99,7 @@ elements: |
     minlength_hidden_dependent:
       '#type': textfield
       '#title': minlength_hidden_dependent
-      '#minlength': 1
+      '#minlength': 5
       '#states':
         visible:
           ':input[name="minlength_hidden_trigger"]':
@@ -121,6 +123,28 @@ elements: |
         required:
           ':input[name="checkboxes_trigger[one]"]':
             checked: true
+  checkboxes_other_trigger_details:
+    '#type': details
+    '#title': checkboxes_other_trigger
+    '#open': true
+    checkboxes_other_trigger:
+      '#type': webform_checkboxes_other
+      '#title': checkboxes_other_trigger
+      '#options':
+        one: One
+        two: Two
+        three: Three
+    checkboxes_other_dependent_required:
+      '#type': textfield
+      '#title': checkboxes_other_dependent_required
+      '#description': '<b>Required:</b> checkboxes_other[one]:checked=true OR checkboxes_other[other]:filled=true'
+      '#states':
+        required:
+          - ':input[name="checkboxes_other_trigger[checkboxes][one]"]':
+              checked: true
+          - or
+          - ':input[name="checkboxes_other_trigger[other]"]':
+              filled: true
   text_format_trigger_details:
     '#type': details
     '#title': text_format_trigger
@@ -332,7 +356,7 @@ elements: |
       '#states':
         required:
           ':input[name="composite_required_trigger"]':
-            'checked': true
+            checked: true
   composite_sub_elements_required_details:
     '#type': details
     '#title': composite_sub_elements_required
@@ -414,6 +438,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -421,6 +446,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -436,22 +462,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -462,6 +503,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -480,9 +522,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -535,4 +579,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_save.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_save.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e01f6cd5b206023f6a83721207b0323f0c83d764
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_save.yml
@@ -0,0 +1,234 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_states_server_save
+title: 'Test: Form API #states save hide/show element'
+description: 'Test Drupal&#39;s #states API hide/show element.'
+category: 'Test: Form API #states'
+elements: |
+  trigger_checkbox:
+    '#type': checkbox
+    '#title': trigger_checkbox
+  dependent_hidden:
+    '#type': hidden
+    '#title': dependent_hidden
+    '#default_value': '{dependent_hidden}'
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+  dependent_checkbox:
+    '#type': checkbox
+    '#title': dependent_checkbox
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+  dependent_value:
+    '#type': value
+    '#title': dependent_value
+    '#value': '{value}'
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+  dependent_textfield:
+    '#type': textfield
+    '#title': dependent_textfield
+    '#default_value': '{dependent_textfield}'
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+  dependent_textfield_multiple:
+    '#type': textfield
+    '#title': dependent_textfield
+    '#multiple': true
+    '#default_value':
+      - '{dependent_textfield}'
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+  dependent_details:
+    '#type': details
+    '#title': dependent_details
+    '#states':
+      visible:
+        ':input[name="trigger_checkbox"]':
+          checked: true
+    dependent_details_textfield:
+      '#type': textfield
+      '#title': dependent_details_textfield
+      '#default_value': '{dependent_details_textfield}'
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: true
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: message
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: true
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 0
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_wizard.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_wizard.yml
similarity index 87%
rename from web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_wizard.yml
rename to web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_wizard.yml
index 91b28dbb9e3b634cd455a17373ab0d4f6eacb0a3..2d8ee71e277bb44d5d874bceaa504a5a6839dd10 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_server_wizard.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_server_wizard.yml
@@ -6,9 +6,11 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
-id: test_form_states_server_wizard
+archive: false
+id: test_states_server_wizard
 title: 'Test: Form API #states server-side wizard pages'
 description: 'Test Drupal''s #states API for wizard pages via server-side.'
 category: 'Test: Form API #states'
@@ -111,7 +113,7 @@ elements: |
     page_01_trigger_checkbox_computed:
       '#type': webform_computed_twig
       '#title': trigger_checkbox
-      '#value': '{{ data.page_01_trigger_checkbox ? ''Yes'' : ''No'' }}'
+      '#template': '{{ data.page_01_trigger_checkbox ? ''Yes'' : ''No'' }}'
     page_02_textfield_required:
       '#type': textfield
       '#title': page_02_textfield_required
@@ -150,6 +152,7 @@ elements: |
       '#type': textfield
       '#title': page_02_textfield_visible
       '#description': '<b>Visible:</b> page_01_trigger_checkbox:checked'
+      '#default_value': '{default_value}'
       '#states':
         visible:
           ':input[name="page_01_trigger_checkbox"]':
@@ -158,24 +161,27 @@ elements: |
       '#type': textfield
       '#title': page_02_textfield_visible_slide
       '#description': '<b>Visible (Slide):</b> page_01_trigger_checkbox:checked'
+      '#default_value': '{default_value}'
       '#states':
-        'visible-slide':
+        visible-slide:
           ':input[name="page_01_trigger_checkbox"]':
             checked: true
     page_02_textfield_invisible:
       '#type': textfield
-      '#title': 'page_02_textfield_invisible'
+      '#title': page_02_textfield_invisible
       '#description': '<b>Invisible:</b> page_01_trigger_checkbox:checked'
+      '#default_value': '{default_value}'
       '#states':
         invisible:
           ':input[name="page_01_trigger_checkbox"]':
             checked: true
     page_02_textfield_invisible_slide:
       '#type': textfield
-      '#title': 'page_02_textfield_invisible_slide'
+      '#title': page_02_textfield_invisible_slide
       '#description': '<b>Invisible (Slide):</b> page_01_trigger_checkbox:checked'
+      '#default_value': '{default_value}'
       '#states':
-        'invisible-slide':
+        invisible-slide:
           ':input[name="page_01_trigger_checkbox"]':
             checked: true
     page_02_checkbox_checked:
@@ -222,6 +228,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -229,6 +236,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -244,22 +252,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -270,6 +293,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -288,9 +312,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -343,4 +369,16 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
-handlers: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 1
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_triggers.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_triggers.yml
similarity index 87%
rename from web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_triggers.yml
rename to web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_triggers.yml
index a55bd34c1017f5e57eea2e866d34db389714619c..3e3998c97fe38760c5b1ba36b0744e0d9357d014 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_form_states_triggers.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_states_triggers.yml
@@ -6,9 +6,11 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
-id: test_form_states_triggers
+archive: false
+id: test_states_triggers
 title: 'Test: Form API #states: Triggers and Operators'
 description: 'Test Drupal''s #states API triggers (empty, checked, value, and collapsed).'
 category: 'Test: Form API #states'
@@ -136,6 +138,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -143,6 +146,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -158,22 +162,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -184,6 +203,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -202,9 +222,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -257,4 +279,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_submission_label.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_submission_label.yml
index 3b3a90136306b84f6ac9d93d4bf15c97e3ab7b5a..959085eacb7f29b19b22230c109961d371a70928 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_submission_label.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_submission_label.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: 1
 template: false
+archive: false
 id: test_submission_label
 title: 'Test: Submission: Label'
 description: 'Test custom submission label'
@@ -25,6 +27,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -32,6 +35,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -47,22 +51,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: 'Submitted by [webform_submission:values:name]'
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -73,6 +92,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -91,9 +111,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -146,4 +168,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_submission_log.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_submission_log.yml
index e91d75fb15318925d09c284db80e9e90ea7e878f..8c399e876889dff5cb850aac67ceda99879fa11c 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_submission_log.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_submission_log.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_submission_log
 title: 'Test: Submission: Logging'
 description: 'Test submission event logging.'
@@ -24,6 +26,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -31,6 +34,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: true
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -46,22 +50,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: true
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -72,6 +91,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: all
   draft_multiple: false
   draft_auto_save: false
@@ -90,9 +110,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -145,4 +167,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_submission_views.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_submission_views.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5c12a3b432208a674657980f483fb06a4c1049f5
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_submission_views.yml
@@ -0,0 +1,204 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test
+open: null
+close: null
+weight: 0
+uid: 1
+template: false
+archive: false
+id: test_submission_views
+title: 'Test: Webform submission views'
+description: ''
+category: ''
+elements: |
+  textfield:
+    '#type': textfield
+    '#title': textfield
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views:
+    admin:
+      view: 'webform_submissions:embed_administer'
+      title: 'Administer submissions'
+      webform_routes:
+        - entity.webform.results_submissions
+      node_routes:
+        - entity.node.webform.results_submissions
+    manage:
+      view: 'webform_submissions:embed_manage'
+      title: 'Manage submissions'
+      webform_routes:
+        - entity.webform.results_submissions
+      node_routes:
+        - entity.node.webform.results_submissions
+    review:
+      view: 'webform_submissions:embed_review'
+      title: 'Review submissions'
+      webform_routes:
+        - entity.webform.results_submissions
+      node_routes:
+        - entity.node.webform.results_submissions
+    user:
+      view: 'webform_submissions:embed_default'
+      title: 'User submissions'
+      webform_routes:
+        - entity.webform.user.drafts
+        - entity.webform.user.submissions
+      node_routes:
+        - entity.node.webform.user.drafts
+        - entity.node.webform.user.submissions
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: all
+  draft_multiple: true
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_token.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_token.yml
index 3c7126a462e2c3d5e9f44381609c00e86987bba9..d12c61d868adc4fbd2d3535c1248e5b3cae73744 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_token.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_token.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: '2000-01-01T00:00:00-05:00'
 close: '2020-01-01T00:00:00-05:00'
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_token
 title: 'Test: Token'
 description: 'Test Webform and Webform Submission tokens'
@@ -29,6 +31,7 @@ elements: |
       Open long: [webform:open:long]
       Close: [webform:close]
       Close long: [webform:close:long]
+      
   webform_submission_tokens:
     '#type': webform_codemirror
     '#mode': text
@@ -59,12 +62,14 @@ elements: |
       URL: [webform_submission:url]
       URL Edit Webform: [webform_submission:url:edit-form]
       UUID: [webform_submission:uuid]
+      
   webform_submission_data_tokens:
     '#type': webform_codemirror
     '#mode': text
     '#title': 'Webform Submission Data Tokens'
     '#default_value': |
       Submission values: [webform_submission:values]
+      
   webform_submission_source_entity_tokens:
     '#type': webform_codemirror
     '#mode': text
@@ -93,6 +98,7 @@ elements: |
       Summary: [webform_submission:source-entity:summary]
       Title: [webform_submission:source-entity:title]
       URL: [webform_submission:source-entity:url]
+      
   webform_submission_node_tokens:
     '#type': webform_codemirror
     '#mode': text
@@ -129,6 +135,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -136,6 +143,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -151,22 +159,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -177,6 +200,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -195,9 +219,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -250,6 +276,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email:
     id: email
@@ -261,23 +291,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
+      from_name: _default
+      subject: _default
       body: '<a href="[webform_submission:update-url]">[webform_submission:update-url]</a>'
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_token_submission_value.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_token_submission_value.yml
index 64eeb9aabf6f863baf0d8d6cf77ba7aa15af461b..71aee7050803a336bbb8755b53cd59fbbc1d8381 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_token_submission_value.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_token_submission_value.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_token_submission_value
 title: 'Test: Token submission value'
 description: 'Test Webform submission values tokens'
@@ -16,18 +18,18 @@ elements: |
   email:
     '#type': email
     '#title': email
-    '#default_value': 'example@example.com'
+    '#default_value': example@example.com
   emails:
     '#type': email
     '#title': emails
     '#multiple': true
     '#default_value':
-      - 'one@example.com'
-      - 'two@example.com'
-      - 'three@example.com'
+      - one@example.com
+      - two@example.com
+      - three@example.com
   user:
     '#type': webform_entity_select
-    '#title': 'user'
+    '#title': user
     '#target_type': user
     '#selection_handler': 'default:user'
     '#selection_settings':
@@ -35,7 +37,7 @@ elements: |
     '#default_value': 1
   users:
     '#type': webform_entity_select
-    '#title': 'user'
+    '#title': user
     '#multiple': true
     '#target_type': user
     '#selection_handler': 'default:user'
@@ -76,8 +78,8 @@ elements: |
     '#type': webform_contact
     '#title': contact
     '#default_value':
-      name: John Smith
-      email: 'john@example.com'
+      name: 'John Smith'
+      email: john@example.com
       address: '10 Main Street'
       city: Springfield
       state_province: Alabama
@@ -89,14 +91,14 @@ elements: |
     '#multiple': true
     '#default_value':
       - name: 'John Smith'
-        email: 'john@example.com'
+        email: john@example.com
         address: '10 Main Street'
         city: Springfield
         state_province: Alabama
         postal_code: 12345
         country: 'United States'
       - name: 'Jane Doe'
-        email: 'jane@example.com'
+        email: jane@example.com
         address: '10 Main Street'
         city: Springfield
         state_province: Alabama
@@ -113,6 +115,18 @@ elements: |
       '#type': textfield
       '#title': last_name
       '#default_value': Smith
+  url:
+    '#type': url
+    '#title': url
+    '#default_value': 'http://example.com?query=param'
+  markup:
+    '#type': textfield
+    '#title': markup
+    '#default_value': '<b>Bold</b> &amp; UPPERCASE'
+  script:
+    '#type': textfield
+    '#title': script
+    '#default_value': '<script>alert(''hi'');</script>'
 css: ''
 javascript: ''
 settings:
@@ -121,6 +135,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -128,6 +143,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -143,22 +159,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -169,6 +200,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -201,6 +233,12 @@ settings:
     <tr><th width="50%">webform_submission:values:users:99:entity:account-name</th><td width="50%">[webform_submission:values:users:99:entity:account-name]</td></tr>
     </table>
     
+    <h3>current-user</h3>
+    <table class="table">
+    <tr><th width="50%">current-user:display-name</th><td width="50%">[current-user:display-name]</td></tr>
+    <tr><th width="50%">current-user:missing</th><td width="50%">[current-user:missing]</td></tr>
+    </table>
+    
     <h3>terms</h3>
     <table class="table">
     <tr><th width="50%">webform_submission:values:term</th><td width="50%">[webform_submission:values:term]</td></tr>
@@ -256,7 +294,25 @@ settings:
     <table class="table">
     <tr><th width="50%">webform_submission:values:missing</th><td width="50%">[webform_submission:values:missing]</td></tr>
     <tr><th width="50%">webform_submission:values:missing:clear</th><td width="50%">[webform_submission:values:missing:clear]</td></tr>
+    <tr><th width="50%">webform:random:missing</th><td width="50%">[webform:random:missing]</td></tr>
+    <tr><th width="50%">webform:random:missing:clear</th><td width="50%">[webform:random:missing:clear]</td></tr>
     </table>
+    
+    <h3>urlencode</h3>
+    <table class="table">
+    <tr><th width="50%">webform_submission:values:url</th><td width="50%">[webform_submission:values:url]</td></tr>
+    <tr><th width="50%">webform_submission:values:url:urlencode</th><td width="50%">[webform_submission:values:url:urlencode]</td></tr>
+    </table>
+    
+    <h3>htmldecode</h3>
+    <table class="table">
+    <tr><th width="50%">webform_submission:values:markup</th><td width="50%">[webform_submission:values:markup]</td></tr>
+    <tr><th width="50%">webform_submission:values:markup:htmldecode</th><td width="50%">[webform_submission:values:markup:htmldecode]</td></tr>
+    <tr><th width="50%">webform_submission:values:markup:htmldecode:striptags</th><td width="50%">[webform_submission:values:markup:htmldecode:striptags]</td></tr>
+    <tr><th width="50%">webform_submission:values:script</th><td width="50%">[webform_submission:values:script]</td></tr>
+    <tr><th width="50%">webform_submission:values:script:htmldecode</th><td width="50%">[webform_submission:values:script:htmldecode]</td></tr>
+    </table>
+    
   confirmation_url: ''
   confirmation_attributes: {  }
   confirmation_back: true
@@ -267,9 +323,11 @@ settings:
   limit_total: 100
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: 10
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: 50
   entity_limit_total_interval: null
   entity_limit_user: 5
@@ -322,4 +380,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_token_update.yml b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_token_update.yml
index afb597d3c11c188f3df86c391fb334fd5d0466e9..53a644eb874f5ba13bf17b4b97b2a4b937f42baf 100644
--- a/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_token_update.yml
+++ b/web/modules/webform/tests/modules/webform_test/config/install/webform.webform.test_token_update.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_token_update
 title: 'Test: Token: Update'
 description: 'Test updating submission using secure token.'
@@ -24,6 +26,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -31,6 +34,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -46,22 +50,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -72,6 +91,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -90,9 +110,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -145,6 +167,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email:
     id: email
@@ -156,23 +182,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
+      from_name: _default
+      subject: _default
       body: '<a href="[webform_submission:update-url]">[webform_submission:update-url]</a>'
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_element_description_tooltip.inc b/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_element_description_tooltip.inc
index aa780e18faa97db566b0f015703362a8ddb29512..e0537f1e8da4fd853d6f3cc26bdd2df21ed4c420 100644
--- a/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_element_description_tooltip.inc
+++ b/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_element_description_tooltip.inc
@@ -5,8 +5,6 @@
  * Generate test elements with with description tooltips.
  */
 
-use Drupal\Component\Utility\Unicode;
-
 /**
  * Generate test element description tooltip.
  *
@@ -35,7 +33,7 @@ function webform_test_test_element_description_tooltip() {
     }
 
     $category_name = (string) $webform_element->getPluginDefinition()['category'] ?: 'Other elements';
-    $category_id = preg_replace('/[^a-zA-Z0-9]+/', '_', Unicode::strtolower($category_name));
+    $category_id = preg_replace('/[^a-zA-Z0-9]+/', '_', mb_strtolower($category_name));
     if (empty($data[$category_id])) {
       $data[$category_id] = [
         '#type' => 'details',
diff --git a/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_element_format.inc b/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_element_format.inc
index a1b92ddbbf92b020a9b6d34b5c1c1af538d03a9b..512c0816a256c2476e3cdd36d34884b3783a285f 100644
--- a/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_element_format.inc
+++ b/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_element_format.inc
@@ -5,7 +5,6 @@
  * Generate test elements with formatting.
  */
 
-use Drupal\Component\Utility\Unicode;
 use Drupal\webform\WebformInterface;
 
 /**
@@ -93,7 +92,7 @@ function webform_test_test_element_format(WebformInterface $webform, $composite
 
     // Set default (test) value.
     if (strpos($element_type, 'date') === 0) {
-      $element['#default_value'] =  '1942-06-18';
+      $element['#default_value'] = ($multiple) ? ['1942-06-18', '1940-07-07', '1943-02-25'] : '1942-06-18';
     }
     elseif ($default_value = $submission_generate->getTestValue($webform, $element_type, $element, ['random' => FALSE])) {
       $element['#default_value'] = $default_value;
@@ -106,7 +105,7 @@ function webform_test_test_element_format(WebformInterface $webform, $composite
 
     // Set element category.
     $category_name = (string) $webform_element->getPluginDefinition()['category'] ?: 'Other elements';
-    $category_id = preg_replace('/[^a-zA-Z0-9]+/', '_', Unicode::strtolower($category_name));
+    $category_id = preg_replace('/[^a-zA-Z0-9]+/', '_', mb_strtolower($category_name));
     if (empty($data[$category_id])) {
       $data[$category_id] = [
         '#type' => 'details',
diff --git a/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_element_icheck_styles.inc b/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_element_icheck_styles.inc
index cd5bc1ec41fd375e317dded8deb33807b25e2f40..3b4cbde0ebdd81f4b04dcf51e88daa39d9e34367 100644
--- a/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_element_icheck_styles.inc
+++ b/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_element_icheck_styles.inc
@@ -61,7 +61,7 @@ function webform_test_test_element_icheck_styles() {
           'three' => 'Three',
         ],
         '#default_value' => 'one',
-        '#wrapper_attributes' => ['class' => 'container-inline'],
+        '#wrapper_attributes' => ['class' => ['container-inline']],
         '#icheck' => $style,
       ];
       $elements[$skin_name][$name . '_radios'] = [
diff --git a/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_example_elements.inc b/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_example_elements.inc
index 835c3f4948d76eae684eb985517c93c324c5586b..d9a823285a519e9e9797f2f4746aa4856c58e250 100644
--- a/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_example_elements.inc
+++ b/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_example_elements.inc
@@ -5,8 +5,6 @@
  * Generate examples of all elements.
  */
 
-use Drupal\Component\Utility\Unicode;
-
 /**
  * Generate examples of all elements.
  *
@@ -38,7 +36,7 @@ function webform_test_test_example_elements() {
     }
 
     $category_name = (string) $webform_element->getPluginDefinition()['category'] ?: 'Other elements';
-    $category_id = preg_replace('/[^a-zA-Z0-9]+/', '_', Unicode::strtolower($category_name));
+    $category_id = preg_replace('/[^a-zA-Z0-9]+/', '_', mb_strtolower($category_name));
     if (empty($data[$category_id])) {
       $data[$category_id] = [
         '#type' => 'details',
diff --git a/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_form_states.inc b/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_form_states.inc
index 18a2841a3df56f9367257bb8a0c8d5862b830ed3..eb86179d37c9232f306d6810790ec798d42a167d 100644
--- a/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_form_states.inc
+++ b/web/modules/webform/tests/modules/webform_test/includes/webform_test.test_form_states.inc
@@ -13,7 +13,7 @@
  * @return array
  *   An array containing test elements with #states.
  */
-function webform_test_test_form_states() {
+function webform_test_test_states() {
   $elements = [];
 
   // Visible.
@@ -25,7 +25,7 @@ function webform_test_test_form_states() {
     '#type' => 'checkbox',
     '#title' => 'Displays and require elements',
   ];
-  $elements['visible'] += _webform_test_form_states('visible', [
+  $elements['visible'] += _webform_test_states('visible', [
     '#states' => [
       'visible' => [
         ':input[name="visible_trigger"]' => [
@@ -49,7 +49,7 @@ function webform_test_test_form_states() {
     '#type' => 'checkbox',
     '#title' => 'Hide and empty elements',
   ];
-  $elements['invisible'] += _webform_test_form_states('invisible', [
+  $elements['invisible'] += _webform_test_states('invisible', [
     '#states' => [
       'invisible' => [
         ':input[name="invisible_trigger"]' => [
@@ -68,7 +68,7 @@ function webform_test_test_form_states() {
     '#type' => 'checkbox',
     '#title' => 'Disable elements',
   ];
-  $elements['disabled'] += _webform_test_form_states('disabled', [
+  $elements['disabled'] += _webform_test_states('disabled', [
     '#states' => [
       'disabled' => [
         ':input[name="disabled_trigger"]' => [
@@ -92,7 +92,7 @@ function webform_test_test_form_states() {
  * @return array
  *   A render array of example elements
  */
-function _webform_test_form_states($type, array $default_properties = []) {
+function _webform_test_states($type, array $default_properties = []) {
   $data = [
     'containers' => [],
     'elements' => [],
diff --git a/web/modules/webform/tests/modules/webform_test/webform_test.info.yml b/web/modules/webform/tests/modules/webform_test/webform_test.info.yml
index 25972c866f2d2ff308ffa722cc97712e108b84e4..ab9a74e2b3d0e5d59ce54ecdf355d1db4bd4392c 100644
--- a/web/modules/webform/tests/modules/webform_test/webform_test.info.yml
+++ b/web/modules/webform/tests/modules/webform_test/webform_test.info.yml
@@ -6,8 +6,8 @@ package: Testing
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test/webform_test.module b/web/modules/webform/tests/modules/webform_test/webform_test.module
index 0bb5af41377d0df63ee48e8a0a10696c1f13335e..a475549d0c5b7a5701d06fd4ee314f1e6d5a31cd 100644
--- a/web/modules/webform/tests/modules/webform_test/webform_test.module
+++ b/web/modules/webform/tests/modules/webform_test/webform_test.module
@@ -5,53 +5,10 @@
  * Support module for webform related testing.
  */
 
-use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Serialization\Yaml;
 use Drupal\Core\Url;
 use Drupal\webform\Utility\WebformElementHelper;
 
-/**
- * Implements hook_webform_submission_form_alter().
- *
- * Adds button to disable/enable HTML client-side validation without have
- * to change any webform settings.
- *
- * The link is only applicable to webform ids that are prefix with test_*.
- */
-function webform_test_webform_submission_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
-  if (!\Drupal::currentUser()->hasPermission('access devel information')) {
-    return;
-  }
-
-  if (strpos($form['#webform_id'], 'test_') !== 0) {
-    return;
-  }
-
-  if (\Drupal::request()->query->get('novalidate') == '1') {
-    $form['#attributes']['novalidate'] = TRUE;
-  }
-  elseif (\Drupal::request()->query->get('novalidate') == '0') {
-    unset($form['#attributes']['novalidate']);
-  }
-
-  if (!empty($form['#attributes']['novalidate'])) {
-    $form['webform_test_novalidate'] = [
-      '#type' => 'link',
-      '#title' => t('Enable client-side validation'),
-      '#url' => Url::fromRoute('<current>', [], ['query' => ['novalidate' => '0']]),
-      '#weight' => 1000,
-    ];
-  }
-  else {
-    $form['webform_test_novalidate'] = [
-      '#type' => 'link',
-      '#title' => t('Disable client-side validation'),
-      '#url' => Url::fromRoute('<current>', [], ['query' => ['novalidate' => '1']]),
-      '#weight' => 1000,
-    ];
-  }
-}
-
 /******************************************************************************/
 // Generate elements.
 /******************************************************************************/
@@ -65,7 +22,7 @@ function webform_test_preprocess_page(&$variables) {
     && _webform_test_load_include(\Drupal::routeMatch()->getRawParameter('webform'))
   ) {
     $t_args = [':href' => Url::fromRouteMatch(\Drupal::routeMatch())->toString() . '?generate'];
-    drupal_set_message(t('The below webform\'s elements are automatically generated and exported. You can regenerate the below elements by appending <a href=":href">?generate</a> to this page\'s URL.', $t_args), 'warning');
+    \Drupal::messenger()->addWarning(t('The below webform\'s elements are automatically generated and exported. You can regenerate the below elements by appending <a href=":href">?generate</a> to this page\'s URL.', $t_args));
   }
 }
 
@@ -94,7 +51,7 @@ function webform_test_webform_load(array $entities) {
         ->save();
 
       // Display message.
-      drupal_set_message(t('Generated elements for %title webform', ['%title' => $entity->label()]));
+      \Drupal::messenger()->addStatus(t('Generated elements for %title webform', ['%title' => $entity->label()]));
     }
   }
 }
diff --git a/web/modules/webform/tests/modules/webform_test_ajax/config/optional/block.block.bartik_webform_test_ajax.yml b/web/modules/webform/tests/modules/webform_test_ajax/config/optional/block.block.bartik_webform_test_ajax.yml
index f3fc5b4ecdb8d45dd3cec74be234afb6d9d7be23..ad72fbc2dbd545791dbc60d6ae1aceeb9905e02f 100644
--- a/web/modules/webform/tests/modules/webform_test_ajax/config/optional/block.block.bartik_webform_test_ajax.yml
+++ b/web/modules/webform/tests/modules/webform_test_ajax/config/optional/block.block.bartik_webform_test_ajax.yml
@@ -17,9 +17,4 @@ settings:
   label: 'Test Webform Ajax Modal Dialogs'
   provider: webform_test_ajax
   label_display: visible
-visibility:
-  request_path:
-    id: request_path
-    pages: '<front>'
-    negate: false
-    context_mapping: {  }
+visibility: {  }
diff --git a/web/modules/webform/tests/modules/webform_test_ajax/src/Plugin/Block/WebformTestAjaxBlock.php b/web/modules/webform/tests/modules/webform_test_ajax/src/Plugin/Block/WebformTestAjaxBlock.php
index 0c32dc7f1172975e3f75141a20172874816b6a05..0352439bc29d03850dcf80e8a0b7c5844618c367 100644
--- a/web/modules/webform/tests/modules/webform_test_ajax/src/Plugin/Block/WebformTestAjaxBlock.php
+++ b/web/modules/webform/tests/modules/webform_test_ajax/src/Plugin/Block/WebformTestAjaxBlock.php
@@ -28,7 +28,7 @@ class WebformTestAjaxBlock extends BlockBase implements ContainerFactoryPluginIn
   protected $redirectDestination;
 
   /**
-   * Creates a WebformBlock instance.
+   * Creates a WebformTestAjaxBlock instance.
    *
    * @param array $configuration
    *   A configuration array containing information about the plugin instance.
@@ -62,7 +62,7 @@ public static function create(ContainerInterface $container, array $configuratio
   public function build() {
     $webforms = Webform::loadMultiple();
 
-    $links = [];
+    $ajax_links = [];
     foreach ($webforms as $webform_id => $webform) {
       if (strpos($webform_id, 'test_ajax') !== 0 && $webform_id != 'test_form_wizard_long_100') {
         continue;
@@ -77,7 +77,7 @@ public function build() {
         $route_options = [];
       }
 
-      $links[$webform_id] = [
+      $ajax_links[$webform_id] = [
         'title' => $this->t('Open @webform_id', ['@webform_id' => $webform_id]),
         'url' => $webform->toUrl('canonical', $route_options),
         'attributes' => [
@@ -92,9 +92,35 @@ public function build() {
       ];
     }
 
+    $webform = Webform::load('contact');
+    $inline_links = [
+      'webform' => [
+        'title' => $this->t('Open Contact'),
+        'url' => $webform->toUrl('canonical'),
+        'attributes' => [
+          'class' => ['webform-dialog', 'webform-dialog-normal'],
+        ],
+      ],
+      'source_entity' => [
+        'title' => $this->t('Open Contact with Source Entity'),
+        'url' => $webform->toUrl('canonical', ['query' => ['source_entity_type' => 'ENTITY_TYPE', 'source_entity_id' => 'ENTITY_ID']]),
+        'attributes' => [
+          'class' => ['webform-dialog', 'webform-dialog-normal'],
+        ],
+      ],
+    ];
+
     return [
-      '#theme' => 'links',
-      '#links' => $links,
+      'ajax' => [
+        '#prefix' => '<h3>' . $this->t('Ajax links') . '</h3>',
+        '#theme' => 'links',
+        '#links' => $ajax_links,
+      ],
+      'inline' => [
+        '#prefix' => '<h3>' . $this->t('Inline (Global) links') . '</h3>',
+        '#theme' => 'links',
+        '#links' => $inline_links,
+      ],
       '#attached' => ['library' => ['core/drupal.ajax']],
     ];
   }
diff --git a/web/modules/webform/tests/modules/webform_test_ajax/webform_test_ajax.info.yml b/web/modules/webform/tests/modules/webform_test_ajax/webform_test_ajax.info.yml
index 7db72a4a01656f69b88c42fd67e4e94f115d3c3b..4871326949f9f1e8777982a4fd92471e6472c16d 100644
--- a/web/modules/webform/tests/modules/webform_test_ajax/webform_test_ajax.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_ajax/webform_test_ajax.info.yml
@@ -7,8 +7,8 @@ dependencies:
   - 'drupal:block'
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_alter_hooks/webform_test_alter_hooks.info.yml b/web/modules/webform/tests/modules/webform_test_alter_hooks/webform_test_alter_hooks.info.yml
index 687771dc0015036b3528b7d50256acaebb72ef96..5b12b96fcc11076a03d7f50b887c99d3565df363 100644
--- a/web/modules/webform/tests/modules/webform_test_alter_hooks/webform_test_alter_hooks.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_alter_hooks/webform_test_alter_hooks.info.yml
@@ -6,8 +6,8 @@ package: Testing
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_alter_hooks/webform_test_alter_hooks.module b/web/modules/webform/tests/modules/webform_test_alter_hooks/webform_test_alter_hooks.module
index 4ffb9187fdc8935b3f1cb03a5a18758b54e4cfb4..159a90552751b0aebbcef996fd30768267bbd509 100644
--- a/web/modules/webform/tests/modules/webform_test_alter_hooks/webform_test_alter_hooks.module
+++ b/web/modules/webform/tests/modules/webform_test_alter_hooks/webform_test_alter_hooks.module
@@ -16,7 +16,7 @@
  */
 function webform_test_alter_hooks_form_alter(&$form, FormStateInterface $form_state, $form_id) {
   if (strpos($form_id, 'webform_') === 0) {
-    drupal_set_message(t("@hook: '@form_id' executed.", ['@hook' => 'hook_form_alter()', '@form_id' => $form_id]), 'status', TRUE);
+    \Drupal::messenger()->addStatus(t("@hook: '@form_id' executed.", ['@hook' => 'hook_form_alter()', '@form_id' => $form_id]), TRUE);
   }
 }
 
@@ -24,7 +24,7 @@ function webform_test_alter_hooks_form_alter(&$form, FormStateInterface $form_st
  * Implements hook_form_webform_submission_form_alter().
  */
 function webform_test_alter_hooks_form_webform_submission_form_alter(array $form, FormStateInterface $form_state, $form_id) {
-  drupal_set_message(t("@hook: '@form_id' executed.", ['@hook' => 'hook_form_webform_submission_form_alter()', '@form_id' => $form_id]), 'status', TRUE);
+  \Drupal::messenger()->addStatus(t("@hook: '@form_id' executed.", ['@hook' => 'hook_form_webform_submission_form_alter()', '@form_id' => $form_id]), TRUE);
 }
 
 /**
@@ -35,7 +35,7 @@ function webform_test_alter_hooks_form_webform_submission_form_alter(array $form
  * @see \Drupal\Core\Form\FormBuilder::prepareForm
  */
 function webform_test_alter_hooks_form_webform_submission_contact_form_alter(array $form, FormStateInterface $form_state, $form_id) {
-  drupal_set_message(t("@hook: '@form_id' executed.", ['@hook' => 'hook_form_webform_submission_BASE_FORM_ID_form_alter()', '@form_id' => $form_id]), 'status', TRUE);
+  \Drupal::messenger()->addStatus(t("@hook: '@form_id' executed.", ['@hook' => 'hook_form_webform_submission_BASE_FORM_ID_form_alter()', '@form_id' => $form_id]), TRUE);
 }
 
 /**
@@ -46,7 +46,7 @@ function webform_test_alter_hooks_form_webform_submission_contact_form_alter(arr
  * @see \Drupal\Core\Form\FormBuilder::prepareForm
  */
 function webform_test_alter_hooks_form_webform_submission_contact_add_form_alter(array $form, FormStateInterface $form_state, $form_id) {
-  drupal_set_message(t("@hook: '@form_id' executed.", ['@hook' => 'hook_form_webform_submission_FORM_ID_form_alter()', '@form_id' => $form_id]), 'status', TRUE);
+  \Drupal::messenger()->addStatus(t("@hook: '@form_id' executed.", ['@hook' => 'hook_form_webform_submission_FORM_ID_form_alter()', '@form_id' => $form_id]), TRUE);
 }
 
 /**
@@ -57,7 +57,7 @@ function webform_test_alter_hooks_form_webform_submission_contact_add_form_alter
  * @see \Drupal\Core\Form\FormBuilder::prepareForm
  */
 function webform_test_alter_hooks_form_webform_submission_contact_node_1_add_form_alter(array $form, FormStateInterface $form_state, $form_id) {
-  drupal_set_message(t("@hook: '@form_id' executed.", ['@hook' => 'hook_form_webform_submission_FORM_ID_form_alter()', '@form_id' => $form_id]), 'status', TRUE);
+  \Drupal::messenger()->addStatus(t("@hook: '@form_id' executed.", ['@hook' => 'hook_form_webform_submission_FORM_ID_form_alter()', '@form_id' => $form_id]), TRUE);
 }
 
 /**
@@ -65,8 +65,8 @@ function webform_test_alter_hooks_form_webform_submission_contact_node_1_add_for
  *
  * @see \Drupal\webform\WebformSubmissionForm::buildForm
  */
-function webform_test_alter_hooks_webform_submission_form_alter(array &$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
-  drupal_set_message(t("@hook: '@form_id' executed.", ['@hook' => 'hook_webform_submission_form_alter()', '@form_id' => $form_id]), 'status', TRUE);
+function webform_test_alter_hooks_webform_submission_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
+  \Drupal::messenger()->addStatus(t("@hook: '@form_id' executed.", ['@hook' => 'hook_webform_submission_form_alter()', '@form_id' => $form_id]), TRUE);
 }
 
 /******************************************************************************/
@@ -80,7 +80,7 @@ function webform_test_alter_hooks_webform_submission_form_alter(array &$form, \D
  * @see \Drupal\webform\WebformSubmissionForm::prepareElements
  */
 function webform_test_alter_hooks_webform_element_alter(array &$element, FormStateInterface $form_state, array $context) {
-  drupal_set_message(t("@hook: '@webform_key' executed.", ['@hook' => 'hook_webform_element_alter()', '@webform_key' => $element['#webform_key']]), 'status', TRUE);
+  \Drupal::messenger()->addStatus(t("@hook: '@webform_key' executed.", ['@hook' => 'hook_webform_element_alter()', '@webform_key' => $element['#webform_key']]), TRUE);
 
 }
 
@@ -91,5 +91,5 @@ function webform_test_alter_hooks_webform_element_alter(array &$element, FormSta
  * @see \Drupal\webform\WebformSubmissionForm::prepareElements
  */
 function webform_test_alter_hooks_webform_element_email_alter(array &$element, FormStateInterface $form_state, array $context) {
-  drupal_set_message(t("@hook: '@webform_key' executed.", ['@hook' => 'hook_webform_element_ELEMENT_TYPE_alter()', '@webform_key' => $element['#webform_key']]), 'status', TRUE);
+  \Drupal::messenger()->addStatus(t("@hook: '@webform_key' executed.", ['@hook' => 'hook_webform_element_ELEMENT_TYPE_alter()', '@webform_key' => $element['#webform_key']]), TRUE);
 }
diff --git a/web/modules/webform/tests/modules/webform_test_block_context/webform_test_block_context.info.yml b/web/modules/webform/tests/modules/webform_test_block_context/webform_test_block_context.info.yml
index 93c27bf201548689da7a5e6371ba05eb27fae424..14673cfa8f888e0e7d2e6e5c5d961eab7e045d62 100644
--- a/web/modules/webform/tests/modules/webform_test_block_context/webform_test_block_context.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_block_context/webform_test_block_context.info.yml
@@ -7,8 +7,8 @@ dependencies:
   - 'drupal:block'
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_block_custom/webform_test_block_custom.info.yml b/web/modules/webform/tests/modules/webform_test_block_custom/webform_test_block_custom.info.yml
index 71f9955c4dea27d72f816ba8e3af14307f7b23da..65fc12c841350c3de374737003574281fe8c2364 100644
--- a/web/modules/webform/tests/modules/webform_test_block_custom/webform_test_block_custom.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_block_custom/webform_test_block_custom.info.yml
@@ -8,8 +8,8 @@ dependencies:
   - 'drupal:block_content'
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_block_submission_limit/webform_test_block_submission_limit.info.yml b/web/modules/webform/tests/modules/webform_test_block_submission_limit/webform_test_block_submission_limit.info.yml
index cfa7e96cdee55622a8c3b6aef8d14a54bfb92b24..7d7a2685060b03059050e3ef23331211bbec436d 100644
--- a/web/modules/webform/tests/modules/webform_test_block_submission_limit/webform_test_block_submission_limit.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_block_submission_limit/webform_test_block_submission_limit.info.yml
@@ -7,8 +7,8 @@ dependencies:
   - 'drupal:block'
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_config_performance/webform_test_config_performance.info.yml b/web/modules/webform/tests/modules/webform_test_config_performance/webform_test_config_performance.info.yml
index f48f1e1c6cb39ef71457fed8d2e0423f6e081602..da55a630d778ecab854671dc69259df374299c74 100644
--- a/web/modules/webform/tests/modules/webform_test_config_performance/webform_test_config_performance.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_config_performance/webform_test_config_performance.info.yml
@@ -6,8 +6,8 @@ package: Testing
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_custom_properties/webform_test_custom_properties.info.yml b/web/modules/webform/tests/modules/webform_test_custom_properties/webform_test_custom_properties.info.yml
index f781244543493599564dfacdd719096c2010921a..993734001e509dc52069486f50f4c1e56486fc3d 100644
--- a/web/modules/webform/tests/modules/webform_test_custom_properties/webform_test_custom_properties.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_custom_properties/webform_test_custom_properties.info.yml
@@ -7,8 +7,8 @@ dependencies:
   - 'webform:webform'
   - 'webform:webform_ui'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_element/config/install/webform.webform.test_element_comp_file_plugin.yml b/web/modules/webform/tests/modules/webform_test_element/config/install/webform.webform.test_element_comp_file_plugin.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6cbf785a83e16da12ea79b745d05ca40d70592fc
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_element/config/install/webform.webform.test_element_comp_file_plugin.yml
@@ -0,0 +1,191 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test_element
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_comp_file_plugin
+title: 'Test: Element: Composite custom file upload plugin'
+description: 'Test custom webform composite element with file upload.'
+category: 'Test: Element'
+elements: |
+  webform_test_composite_file:
+    '#type': webform_test_composite_file
+    '#title': webform_test_composite_file
+  webform_test_composite_file_multiple:
+    '#type': webform_test_composite_file
+    '#title': webform_test_composite_file_multiple
+    '#multiple': true
+  webform_test_composite_file_multiple_header:
+    '#type': webform_test_composite_file
+    '#title': webform_test_composite_file_multiple_header
+    '#multiple': true
+    '#multiple__header': true
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 0
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test_element/config/install/webform.webform.test_element_composite_plugin.yml b/web/modules/webform/tests/modules/webform_test_element/config/install/webform.webform.test_element_composite_plugin.yml
index 650259d2e63a5465c538efd70bdc31f2be1e680e..3af15449b0df468fa9ec800a3ec6c0f5f399f4ff 100644
--- a/web/modules/webform/tests/modules/webform_test_element/config/install/webform.webform.test_element_composite_plugin.yml
+++ b/web/modules/webform/tests/modules/webform_test_element/config/install/webform.webform.test_element_composite_plugin.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test_element
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_composite_plugin
 title: 'Test: Element: Composite custom plugin'
 description: 'Test custom webform composite element.'
@@ -33,6 +35,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -40,6 +43,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -55,22 +59,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -81,6 +100,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -99,9 +119,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -154,6 +176,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   debug:
     id: debug
diff --git a/web/modules/webform/tests/modules/webform_test_element/config/install/webform.webform.test_element_plugin.yml b/web/modules/webform/tests/modules/webform_test_element/config/install/webform.webform.test_element_plugin.yml
index 775f734d19836f95e5f76f1cba7dbb25108c5172..3c7dd25bd91fe60937f33ec4444fea22321f5217 100644
--- a/web/modules/webform/tests/modules/webform_test_element/config/install/webform.webform.test_element_plugin.yml
+++ b/web/modules/webform/tests/modules/webform_test_element/config/install/webform.webform.test_element_plugin.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test_element
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_element_plugin
 title: 'Test: Element: Test (plugin)'
 description: 'Test the webform element plugin integration and method invoking.'
@@ -16,6 +18,7 @@ elements: |
   description:
     '#markup': |
       <p>This webform includes a #test elememt which will trigger all methods associated with a WebformElement plugin.</p>
+      
   test:
     '#type': webform_test_element
     '#title': 'This is a test element'
@@ -28,6 +31,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -35,6 +39,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -50,22 +55,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -76,6 +96,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -94,9 +115,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -149,4 +172,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test_element/src/Element/WebformTestComposite.php b/web/modules/webform/tests/modules/webform_test_element/src/Element/WebformTestComposite.php
index 56f61e0f06dfc945a325f33147b4619481245139..1036f67ce2163882c7a77f3c8262ee4c8b82ee26 100644
--- a/web/modules/webform/tests/modules/webform_test_element/src/Element/WebformTestComposite.php
+++ b/web/modules/webform/tests/modules/webform_test_element/src/Element/WebformTestComposite.php
@@ -110,7 +110,6 @@ public static function getCompositeElements(array $element) {
     // @see \Drupal\webform\Element\WebformCompositeBase::processWebformComposite
     // $elements['checkboxes'] = ['#type' => 'checkboxes'];
     // $elements['likert'] = ['#type' => 'webform_likert'];
-    // $elements['likert'] = ['#type' => 'managed_file'];
     // $elements['datetime'] = ['#type' => 'datetime'];
     return $elements;
   }
diff --git a/web/modules/webform/tests/modules/webform_test_element/src/Element/WebformTestCompositeFile.php b/web/modules/webform/tests/modules/webform_test_element/src/Element/WebformTestCompositeFile.php
new file mode 100644
index 0000000000000000000000000000000000000000..9b467e94fc37285f620728165e5a075b22fd5007
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_element/src/Element/WebformTestCompositeFile.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\webform_test_element\Element;
+
+use Drupal\webform\Element\WebformCompositeBase;
+
+/**
+ * Provides a webform composite element file for testing.
+ *
+ * @FormElement("webform_test_composite_file")
+ */
+class WebformTestCompositeFile extends WebformCompositeBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getCompositeElements(array $element) {
+    $elements = [];
+    $elements['textfield'] = [
+      '#type' => 'textfield',
+      '#title' => t('textfield'),
+    ];
+    $elements['managed_file'] = [
+      '#type' => 'managed_file',
+      '#title' => 'managed_file',
+    ];
+    return $elements;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preview() {
+    return [];
+  }
+
+}
diff --git a/web/modules/webform/tests/modules/webform_test_element/src/Plugin/WebformElement/WebformTestCompositeFile.php b/web/modules/webform/tests/modules/webform_test_element/src/Plugin/WebformElement/WebformTestCompositeFile.php
new file mode 100644
index 0000000000000000000000000000000000000000..70878518dc9e79190b0d2fb0456ce307c618f86a
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_element/src/Plugin/WebformElement/WebformTestCompositeFile.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\webform_test_element\Plugin\WebformElement;
+
+use Drupal\webform\Plugin\WebformElement\WebformCompositeBase;
+
+/**
+ * Provides a 'webform_test_composite_file' element.
+ *
+ * @WebformElement(
+ *   id = "webform_test_composite_file",
+ *   label = @Translation("Test composite element file"),
+ *   description = @Translation("Provides a Webform composite file element for testing."),
+ *   multiline = TRUE,
+ *   composite = TRUE,
+ *   states_wrapper = TRUE,
+ * )
+ */
+class WebformTestCompositeFile extends WebformCompositeBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preview() {
+    return [];
+  }
+
+}
diff --git a/web/modules/webform/tests/modules/webform_test_element/src/Plugin/WebformElement/WebformTestElement.php b/web/modules/webform/tests/modules/webform_test_element/src/Plugin/WebformElement/WebformTestElement.php
index 5229257bd3440e4a6830ef6a39c51cebb4de453d..e4709b39afef64f51335706e13241f3aa76686a3 100644
--- a/web/modules/webform/tests/modules/webform_test_element/src/Plugin/WebformElement/WebformTestElement.php
+++ b/web/modules/webform/tests/modules/webform_test_element/src/Plugin/WebformElement/WebformTestElement.php
@@ -123,7 +123,7 @@ public function postSave(array &$element, WebformSubmissionInterface $webform_su
   protected function displayMessage($method_name, $context1 = NULL) {
     if (PHP_SAPI != 'cli') {
       $t_args = ['@class_name' => get_class($this), '@method_name' => $method_name, '@context1' => $context1];
-      drupal_set_message($this->t('Invoked: @class_name:@method_name @context1', $t_args));
+      $this->messenger()->addStatus($this->t('Invoked: @class_name:@method_name @context1', $t_args));
     }
   }
 
@@ -131,7 +131,7 @@ protected function displayMessage($method_name, $context1 = NULL) {
    * Form API callback. Convert password confirm array to single value.
    */
   public static function validate(array &$element, FormStateInterface $form_state) {
-    drupal_set_message(t('Invoked: Drupal\webform_test_element\Plugin\WebformElement\WebformTestElement::validate'));
+    \Drupal::messenger()->addStatus(t('Invoked: Drupal\webform_test_element\Plugin\WebformElement\WebformTestElement::validate'));
   }
 
 }
diff --git a/web/modules/webform/tests/modules/webform_test_element/src/Plugin/WebformElement/WebformTestElementProperties.php b/web/modules/webform/tests/modules/webform_test_element/src/Plugin/WebformElement/WebformTestElementProperties.php
index 54023fc036ec035fa9ea84932ac424685516ea49..3d4fe3d3d43bf8f7e40ec60d8076996fc14741cd 100644
--- a/web/modules/webform/tests/modules/webform_test_element/src/Plugin/WebformElement/WebformTestElementProperties.php
+++ b/web/modules/webform/tests/modules/webform_test_element/src/Plugin/WebformElement/WebformTestElementProperties.php
@@ -106,12 +106,12 @@ public function form(array $form, FormStateInterface $form_state) {
         'wrapper' => $ajax_wrapper_id,
         'progress' => ['type' => 'fullscreen'],
       ],
-      // Hide button, add submit button trigger class, and disable validation.
+      // Disable validation, hide button, add submit button trigger class.
       '#attributes' => [
+        'formnovalidate' => 'formnovalidate',
         'class' => [
           'js-hide',
           'js-webform-test-properties-submit',
-          'js-webform-novalidate',
         ],
       ],
     ];
diff --git a/web/modules/webform/tests/modules/webform_test_element/webform_test_element.info.yml b/web/modules/webform/tests/modules/webform_test_element/webform_test_element.info.yml
index 7ea6526329e86d713c7a7a855a0e8e4389815143..4f87a139c4433f3e221260104804e5fd80ebd8b5 100644
--- a/web/modules/webform/tests/modules/webform_test_element/webform_test_element.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_element/webform_test_element.info.yml
@@ -6,8 +6,8 @@ package: Testing
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_views/config/install/views.view.webform_test_entity_reference.yml b/web/modules/webform/tests/modules/webform_test_entity_reference/config/install/views.view.webform_test_entity_reference_vs.yml
similarity index 56%
rename from web/modules/webform/tests/modules/webform_test_views/config/install/views.view.webform_test_entity_reference.yml
rename to web/modules/webform/tests/modules/webform_test_entity_reference/config/install/views.view.webform_test_entity_reference_vs.yml
index 7a1f8f1a6ccb10d8b0cde6c58dbedd5cde449d03..670fafac6816ad2b1c17c2b1ca8f82f9a3439640 100644
--- a/web/modules/webform/tests/modules/webform_test_views/config/install/views.view.webform_test_entity_reference.yml
+++ b/web/modules/webform/tests/modules/webform_test_entity_reference/config/install/views.view.webform_test_entity_reference_vs.yml
@@ -1,14 +1,16 @@
 langcode: en
 status: true
 dependencies:
-  enforced:
-    module:
-      - webform_test_views
+  config:
+    - field.storage.node.field_image
+    - image.style.thumbnail
+    - node.type.article
   module:
+    - image
     - node
     - user
-id: webform_test_entity_reference
-label: 'Webform test entity reference'
+id: webform_test_entity_reference_vs
+label: 'Webform test entity reference views'
 module: views
 description: ''
 tag: ''
@@ -80,6 +82,70 @@ display:
           hide_empty: false
           default_field_elements: true
       fields:
+        field_image:
+          id: field_image
+          table: node__field_image
+          field: field_image
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: target_id
+          type: image
+          settings:
+            image_style: thumbnail
+            image_link: ''
+          group_column: ''
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          plugin_id: field
         title:
           id: title
           table: node_field_data
@@ -139,6 +205,46 @@ display:
           expose:
             operator: ''
           group: 1
+        type:
+          id: type
+          table: node_field_data
+          field: type
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: in
+          value:
+            article: article
+          group: 1
+          exposed: false
+          expose:
+            operator_id: ''
+            label: ''
+            description: ''
+            use_operator: false
+            operator: ''
+            identifier: ''
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+            reduce: false
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          entity_type: node
+          entity_field: type
+          plugin_id: bundle
       sorts:
         created:
           id: created
@@ -169,10 +275,13 @@ display:
         - url.query_args
         - 'user.node_grants:view'
         - user.permissions
-      tags: {  }
-  entity_reference:
+      tags:
+        - 'config:field.storage.node.field_image'
+        - env
+        - extensions
+  entity_reference_1:
     display_plugin: entity_reference
-    id: entity_reference
+    id: entity_reference_1
     display_title: 'Entity Reference'
     position: 1
     display_options:
@@ -182,19 +291,27 @@ display:
         options:
           search_fields:
             title: title
+            field_image: '0'
+      pager:
+        type: some
+        options:
+          items_per_page: 50
+          offset: 0
       row:
         type: entity_reference
         options:
           default_field_elements: true
-          inline:
-            title: title
-          separator: '-'
+          inline: {  }
+          separator: ''
           hide_empty: false
+      rendering_language: '***LANGUAGE_language_interface***'
     cache_metadata:
       max-age: -1
       contexts:
-        - 'languages:language_content'
         - 'languages:language_interface'
         - 'user.node_grants:view'
         - user.permissions
-      tags: {  }
+      tags:
+        - 'config:field.storage.node.field_image'
+        - env
+        - extensions
diff --git a/web/modules/webform/tests/modules/webform_test_entity_reference/config/install/webform.webform.test_element_entity_reference_vs.yml b/web/modules/webform/tests/modules/webform_test_entity_reference/config/install/webform.webform.test_element_entity_reference_vs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..629f35790961b987e3ccca3f3bbfd21f18a08ec6
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_entity_reference/config/install/webform.webform.test_element_entity_reference_vs.yml
@@ -0,0 +1,229 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test_entity_reference_views
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_element_entity_reference_vs
+title: 'Test: Element: Entity reference views'
+description: 'Test entity reference views elements.'
+category: 'Test: Element'
+elements: |
+  entity_autocomplete:
+    '#type': entity_autocomplete
+    '#title': entity_autocomplete
+    '#multiple': true
+    '#target_type': node
+    '#selection_handler': views
+    '#selection_settings':
+      view:
+        view_name: webform_test_entity_reference_vs
+        display_name: entity_reference_1
+        arguments: {  }
+  entity_select:
+    '#type': webform_entity_select
+    '#title': webform_entity_select
+    '#multiple': true
+    '#target_type': node
+    '#selection_handler': views
+    '#selection_settings':
+      view:
+        view_name: webform_test_entity_reference_vs
+        display_name: entity_reference_1
+        arguments: {  }
+  entity_radios:
+    '#type': webform_entity_radios
+    '#title': webform_entity_radios
+    '#multiple': true
+    '#target_type': node
+    '#selection_handler': views
+    '#selection_settings':
+      view:
+        view_name: webform_test_entity_reference_vs
+        display_name: entity_reference_1
+        arguments: {  }
+    '#wrapper_attributes':
+      class:
+        - webform-entity-reference-options
+  entity_checkboxes:
+    '#type': webform_entity_checkboxes
+    '#title': entity_checkboxes
+    '#multiple': true
+    '#target_type': node
+    '#selection_handler': views
+    '#selection_settings':
+      view:
+        view_name: webform_test_entity_reference_vs
+        display_name: entity_reference_1
+        arguments: {  }
+    '#wrapper_attributes':
+      class:
+        - webform-entity-reference-options
+css: "/* Autocomplete */\r\n\r\n.ui-autocomplete {\r\n  display: flex;\r\n  flex-wrap: wrap !important;\r\n  align-self: flex-start !important;\r\n  max-width: 540px !important;\r\n}\r\n\r\n.ui-autocomplete .ui-menu-item-wrapper {\r\n  display: block !important;\r\n  border: 1px solid #ccc !important;\r\n  background-color: #eee !important;\r\n  width: 100px !important;\r\n  margin: 10px 0 0 10px !important;\r\n  padding: 10px !important;\r\n}\r\n\r\n.ui-autocomplete .ui-menu-item-wrapper.ui-state-active {\r\n  background-color: blue !important;\r\n}\r\n\r\n.ui-autocomplete .views-field views-field-field-image {\r\n  display: block;\r\n  margin: 0 0 5px 0;\r\n}\r\n\r\n/* Checkboex and radios */\r\n\r\n.webform-entity-reference-options .fieldset-wrapper > div {\r\n  display: flex;\r\n  flex-wrap: wrap;\r\n  align-self: flex-start;\r\n}\r\n\r\n.webform-entity-reference-options .form-item {\r\n  position: relative;\r\n}\r\n\r\n.webform-entity-reference-options .form-item input {\r\n  position: absolute;\r\n  top: 20px;\r\n  left: 20px; \r\n}\r\n\r\n.webform-entity-reference-options .form-item label {\r\n  display: block;\r\n  border: 1px solid #ccc;\r\n  background-color: #eee;\r\n  width: 100px;\r\n  margin: 0 10px 10px 0;  \r\n  padding: 10px;  \r\n}\r\n\r\n.webform-entity-reference-options .form-item input:checked + label {\r\n  display: block;\r\n  border: 1px solid #ccc;\r\n  background-color: #ffc;\r\n  width: 100px;\r\n  margin: 0 10px 10px 0;  \r\n  padding: 10px;  \r\n}\r\n\r\n.webform-entity-reference-options label.option img {\r\n  display: block;\r\n  margin: 0 0 5px 0;  \r\n}"
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: false
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 1
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: none
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: page
+  confirmation_title: ''
+  confirmation_message: ''
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: true
+    conditions: {  }
+    weight: 0
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test_entity_reference/webform_test_entity_reference_views.info.yml b/web/modules/webform/tests/modules/webform_test_entity_reference/webform_test_entity_reference_views.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b273138c30bdf2602d8c006110375b60ed57690f
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_entity_reference/webform_test_entity_reference_views.info.yml
@@ -0,0 +1,16 @@
+name: 'Webform module entity reference tests'
+type: module
+description: 'Support module for Webform module entity reference testing.'
+package: Testing
+# core: 8.x
+dependencies:
+  - 'drupal:node'
+  - 'drupal:user'
+  - 'drupal:views'
+  - 'webform:webform'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_exporter/src/Plugin/WebformExporter/TestWebformExporter.php b/web/modules/webform/tests/modules/webform_test_exporter/src/Plugin/WebformExporter/TestWebformExporter.php
new file mode 100644
index 0000000000000000000000000000000000000000..7d200f7a1aa7fb1ec129d1bfddba1d7a69c22322
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_exporter/src/Plugin/WebformExporter/TestWebformExporter.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\webform_test_exporter\Plugin\WebformExporter;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\webform\Plugin\WebformExporter\TableWebformExporter;
+use Drupal\webform\WebformSubmissionInterface;
+
+/**
+ * Defines a test exporter.
+ *
+ * @WebformExporter(
+ *   id = "test",
+ *   label = @Translation("Test"),
+ *   description = @Translation("Test exporter results as an HTML table in reverse column order."),
+ * )
+ */
+class TestWebformExporter extends TableWebformExporter {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return parent::defaultConfiguration() + [
+      'reverse' => TRUE,
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildConfigurationForm($form, $form_state);
+    $form['reverse'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t("Reverse the table's column order"),
+      '#return_value' => TRUE,
+      '#default_value' => $this->configuration['reverse'],
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildHeader() {
+    $header = parent::buildHeader();
+    return ($this->configuration['reverse']) ? array_reverse($header) : $header;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildRecord(WebformSubmissionInterface $webform_submission) {
+    $record = parent::buildRecord($webform_submission);
+    return ($this->configuration['reverse']) ? array_reverse($record) : $record;
+  }
+
+}
diff --git a/web/modules/webform/tests/modules/webform_test_exporter/webform_test_exporter.info.yml b/web/modules/webform/tests/modules/webform_test_exporter/webform_test_exporter.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..173f2efbfc61e1f72cc7fc773b6880a88f3b90de
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_exporter/webform_test_exporter.info.yml
@@ -0,0 +1,13 @@
+name: 'Webform module exporter tests'
+type: module
+description: 'Support module for webform that provides exporter plugin tests.'
+package: Testing
+# core: 8.x
+dependencies:
+  - 'webform:webform'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_handler/config/install/webform.webform.test_handler_conditions.yml b/web/modules/webform/tests/modules/webform_test_handler/config/install/webform.webform.test_handler_conditions.yml
index 1fb22fdb9ce333a5293790ef9bdf715f1548fe71..91a89ea93b99108085df5ca54ea41aedc161037b 100644
--- a/web/modules/webform/tests/modules/webform_test_handler/config/install/webform.webform.test_handler_conditions.yml
+++ b/web/modules/webform/tests/modules/webform_test_handler/config/install/webform.webform.test_handler_conditions.yml
@@ -8,8 +8,10 @@ dependencies:
     - webform_test_handler
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_conditions
 title: 'Test: Handler: Test conditions'
 description: 'Test handler conditions.'
@@ -21,6 +23,7 @@ elements: |
   trigger_b:
     '#type': checkbox
     '#title': trigger_b
+  
 css: ''
 javascript: ''
 settings:
@@ -29,6 +32,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -36,6 +40,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -51,22 +56,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -77,6 +97,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -95,9 +116,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -150,6 +173,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   test_a:
     id: test
diff --git a/web/modules/webform/tests/modules/webform_test_handler/config/install/webform.webform.test_handler_test.yml b/web/modules/webform/tests/modules/webform_test_handler/config/install/webform.webform.test_handler_test.yml
index 9e526e8522cb7e2f91b10d59f141a712a2bd6f95..499ff6a6ed2131acbedabfeb73cfceb930a07bf7 100644
--- a/web/modules/webform/tests/modules/webform_test_handler/config/install/webform.webform.test_handler_test.yml
+++ b/web/modules/webform/tests/modules/webform_test_handler/config/install/webform.webform.test_handler_test.yml
@@ -8,8 +8,10 @@ dependencies:
     - webform_test_handler
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_test
 title: 'Test: Handler: Test invoke methods'
 description: 'Test invoking of webform handler methods.'
@@ -19,6 +21,7 @@ elements: |
     '#type': textfield
     '#title': 'Empty element'
     '#description': 'Entering any value will throw an error'
+  
 css: ''
 javascript: ''
 settings:
@@ -27,6 +30,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -34,6 +38,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -49,22 +54,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -75,6 +95,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -93,9 +114,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -148,6 +171,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   test:
     id: test
@@ -158,3 +185,11 @@ handlers:
     weight: 1
     settings:
       message: 'One two one two this is just a test'
+  debug:
+    id: debug
+    label: Debug
+    handler_id: debug
+    status: false
+    conditions: {  }
+    weight: 2
+    settings: {  }
diff --git a/web/modules/webform/tests/modules/webform_test_handler/config/schema/webform_test_handler.webform.schema.yml b/web/modules/webform/tests/modules/webform_test_handler/config/schema/webform_test_handler.webform.schema.yml
index f38347823873ecb162a84dba6a1bb029ec84e6ac..81017d2dab6ec1b992bf96e3659b47bd02eed831 100644
--- a/web/modules/webform/tests/modules/webform_test_handler/config/schema/webform_test_handler.webform.schema.yml
+++ b/web/modules/webform/tests/modules/webform_test_handler/config/schema/webform_test_handler.webform.schema.yml
@@ -1,13 +1,10 @@
-# Schema for the configuration files of the webform test handler module.
-
 webform.handler.test:
   type: mapping
-  label: 'Test'
+  label: Test
   mapping:
     message:
-      label: 'Message'
+      label: Message
       type: string
-
 webform.handler.test_entity_mapping:
   type: mapping
   label: 'Test entity mapping'
@@ -16,8 +13,8 @@ webform.handler.test_entity_mapping:
       label: 'Entity type'
       type: string
     bundle:
-      label: 'Bundle'
+      label: Bundle
       type: string
     fields:
       type: ignore
-      label: 'Fields'
+      label: Fields
diff --git a/web/modules/webform/tests/modules/webform_test_handler/src/Plugin/WebformHandler/TestEntityMappingWebformHandler.php b/web/modules/webform/tests/modules/webform_test_handler/src/Plugin/WebformHandler/TestEntityMappingWebformHandler.php
index 089bc2d4b00ba6211757ac1308ec9c97d637850c..de5e365a94d034452b3972df17f677d548f76745 100644
--- a/web/modules/webform/tests/modules/webform_test_handler/src/Plugin/WebformHandler/TestEntityMappingWebformHandler.php
+++ b/web/modules/webform/tests/modules/webform_test_handler/src/Plugin/WebformHandler/TestEntityMappingWebformHandler.php
@@ -115,7 +115,6 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
     $form['container']['bundle'] = [
       '#type' => 'select',
       '#title' => $this->t('Bundles'),
-      '#parents' => ['settings', 'bundle'],
       '#default_value' => $this->configuration['bundle'],
       '#options' => $bundle_options,
       '#ajax' => $ajax,
@@ -147,12 +146,11 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
       '#description_display' => 'before',
       '#default_value' => $this->configuration['fields'],
       '#required' => TRUE,
-      '#parents' => ['settings', 'fields'],
       '#source' => $element_options,
       '#destination' => $field_options,
     ];
 
-    return $form;
+    return $this->setSettingsParents($form);
   }
 
   /**
diff --git a/web/modules/webform/tests/modules/webform_test_handler/src/Plugin/WebformHandler/TestWebformHandler.php b/web/modules/webform/tests/modules/webform_test_handler/src/Plugin/WebformHandler/TestWebformHandler.php
index 782445e293be8185811dd5e4c8c12d70c5957e7a..3623a72e11f945a77075c4163156aaf703f1a99c 100644
--- a/web/modules/webform/tests/modules/webform_test_handler/src/Plugin/WebformHandler/TestWebformHandler.php
+++ b/web/modules/webform/tests/modules/webform_test_handler/src/Plugin/WebformHandler/TestWebformHandler.php
@@ -94,7 +94,7 @@ public function submitForm(array &$form, FormStateInterface $form_state, Webform
    * {@inheritdoc}
    */
   public function confirmForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) {
-    drupal_set_message($this->configuration['message'], 'status', TRUE);
+    $this->messenger()->addStatus($this->configuration['message'], TRUE);
     \Drupal::logger('webform.test_form')->notice($this->configuration['message']);
     $this->displayMessage(__FUNCTION__);
   }
@@ -214,7 +214,7 @@ protected function displayMessage($method_name, $context1 = NULL) {
         '@method_name' => $method_name,
         '@context1' => $context1,
       ];
-      drupal_set_message($this->t('Invoked @id: @class_name:@method_name @context1', $t_args), 'status', TRUE);
+      $this->messenger()->addStatus($this->t('Invoked @id: @class_name:@method_name @context1', $t_args), TRUE);
       \Drupal::logger('webform.test_form')->notice('Invoked: @class_name:@method_name @context1', $t_args);
     }
   }
diff --git a/web/modules/webform/tests/modules/webform_test_handler/webform_test_handler.info.yml b/web/modules/webform/tests/modules/webform_test_handler/webform_test_handler.info.yml
index b294720ae2e7241a67c45bd950a31bc81cdb1156..c76a9d3b205b71b1c18e213d11f02f0297862f25 100644
--- a/web/modules/webform/tests/modules/webform_test_handler/webform_test_handler.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_handler/webform_test_handler.info.yml
@@ -6,8 +6,8 @@ package: Testing
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_handler_invoke_alter/webform_test_handler_invoke_alter.info.yml b/web/modules/webform/tests/modules/webform_test_handler_invoke_alter/webform_test_handler_invoke_alter.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..69b5cae8ffa68e20b8d3902e9b64053b8507871c
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_handler_invoke_alter/webform_test_handler_invoke_alter.info.yml
@@ -0,0 +1,13 @@
+name: 'Webform module handler invoke alter tests'
+type: module
+description: 'Support module for webform that provides handler invoke alter tests.'
+package: Testing
+# core: 8.x
+dependencies:
+  - 'webform:webform'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_handler_invoke_alter/webform_test_handler_invoke_alter.module b/web/modules/webform/tests/modules/webform_test_handler_invoke_alter/webform_test_handler_invoke_alter.module
new file mode 100644
index 0000000000000000000000000000000000000000..db54daa4e36eb1033d2c4a5d6eb9203a30c3219c
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_handler_invoke_alter/webform_test_handler_invoke_alter.module
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Support module for webform that provides handler invoke alter tests.
+ */
+
+use Drupal\webform\Plugin\WebformHandlerInterface;
+
+/**
+ * Implements hook_webform_handler_invoke_alter().
+ */
+function webform_test_handler_invoke_alter_webform_handler_invoke_alter(WebformHandlerInterface $handler, $method_name, array $args) {
+  $t_args = [
+    '@webform_id' => $handler->getWebform()->id(),
+    '@handler_id' => $handler->getHandlerId(),
+    '@method_name' => $method_name,
+  ];
+  \Drupal::messenger()->addStatus(t('Invoking hook_webform_handler_invoke_alter() for "@webform_id:@handler_id::@method_name"', $t_args), TRUE);
+}
+
+/**
+ * Implements hook_webform_handler_invoke_METHOD_NAME_alter().
+ */
+function webform_test_handler_invoke_alter_webform_handler_invoke_pre_create_alter(WebformHandlerInterface $handler, array $args) {
+  $t_args = [
+    '@webform_id' => $handler->getWebform()->id(),
+    '@handler_id' => $handler->getHandlerId(),
+  ];
+  \Drupal::messenger()->addStatus(t('Invoking hook_webform_handler_invoke_pre_create_alter() for "@webform_id:@handler_id"', $t_args), TRUE);
+}
diff --git a/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_get.yml b/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_get.yml
index 412f041673b777093d4aba8084d2db507dc0c3db..8bf1befcbd86f46dee467283e75f47d20072f7f4 100644
--- a/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_get.yml
+++ b/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_get.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test_handler_remote_post
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_remote_get
 title: 'Test: Handler: Remote HTTP GET'
 description: 'Test remote post handler using HTTP GET method.'
@@ -35,6 +37,7 @@ elements: |
     '#title': 'Confirmation number'
     '#type': value
     '#value': '[webform:handler:remote_post:completed:confirmation_number]'
+  
 css: ''
 javascript: ''
 settings:
@@ -43,6 +46,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -50,6 +54,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: true
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -65,22 +70,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -91,6 +111,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: all
   draft_multiple: false
   draft_auto_save: false
@@ -100,6 +121,7 @@ settings:
   confirmation_title: ''
   confirmation_message: |
     <p>Your confirmation number is [webform_submission:values:confirmation_number].</p>
+    
   confirmation_url: ''
   confirmation_attributes: {  }
   confirmation_back: true
@@ -110,9 +132,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -165,6 +189,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   remote_post:
     id: remote_post
@@ -199,24 +227,31 @@ handlers:
         confirmation_number: confirmation_number
       custom_data: |
         custom_data: true
+        
       custom_options: |
         headers:
           custom_header: 'true'
+        
       debug: true
       completed_url: 'http://webform-test-handler-remote-post/completed'
       completed_custom_data: |
         custom_completed: true
+        
       updated_url: 'http://webform-test-handler-remote-post/updated'
       updated_custom_data: |
         custom_updated: true
+        
       deleted_url: 'http://webform-test-handler-remote-post/deleted'
       deleted_custom_data: |
         custom_deleted: true
+        
       draft_url: 'http://webform-test-handler-remote-post/draft'
       draft_custom_data: |
         custom_draft: true
+        
       converted_url: 'http://webform-test-handler-remote-post/converted'
       converted_custom_data: |
         custom_converted: true
+        
       message: ''
-      messages: { }
+      messages: {  }
diff --git a/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_post.yml b/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_post.yml
index c384595a7571ffdb6030c6ea6303ae497c5fadfb..35464c3f3ae52ce470bc1c83879b375a0c674bb1 100644
--- a/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_post.yml
+++ b/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_post.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test_handler_remote_post
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_remote_post
 title: 'Test: Handler: Remote post'
 description: 'Test remote post handler.'
@@ -36,6 +38,7 @@ elements: |
     '#title': 'Confirmation number'
     '#type': value
     '#value': '[webform:handler:remote_post:completed:confirmation_number]'
+  
 css: ''
 javascript: ''
 settings:
@@ -44,6 +47,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -51,6 +55,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: true
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -66,22 +71,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -92,6 +112,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: all
   draft_multiple: false
   draft_auto_save: false
@@ -101,6 +122,7 @@ settings:
   confirmation_title: ''
   confirmation_message: |
     <p>Your confirmation number is [webform_submission:values:confirmation_number].</p>
+    
   confirmation_url: ''
   confirmation_attributes: {  }
   confirmation_back: true
@@ -111,9 +133,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -166,6 +190,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   remote_post:
     id: remote_post
@@ -200,25 +228,33 @@ handlers:
         confirmation_number: confirmation_number
       custom_data: |
         custom_data: true
+        
       custom_options: |
         headers:
+          'Accept-Language': '[webform_submission:langcode]'
           custom_header: 'true'
+        
       debug: true
       completed_url: 'http://webform-test-handler-remote-post/completed'
       completed_custom_data: |
         custom_completed: true
+        
       updated_url: 'http://webform-test-handler-remote-post/updated'
       updated_custom_data: |
         custom_updated: true
+        
       deleted_url: 'http://webform-test-handler-remote-post/deleted'
       deleted_custom_data: |
         custom_deleted: true
+        
       draft_url: 'http://webform-test-handler-remote-post/draft'
       draft_custom_data: |
         custom_draft: true
+        
       converted_url: 'http://webform-test-handler-remote-post/converted'
       converted_custom_data: |
         custom_converted: true
+        
       message: ''
       messages:
         - code: 401
diff --git a/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_post_file.yml b/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_post_file.yml
index c7c31b19a278f58f875242ed231157b8c4652f10..b94f477d6de5cf349663ffef5505be82e0f4037e 100644
--- a/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_post_file.yml
+++ b/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_post_file.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test_handler_remote_post
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_handler_remote_post_file
 title: 'Test: Handler: Remote post file'
 description: 'Test remote post handler file.'
@@ -18,6 +20,13 @@ elements: |
     '#type': managed_file
     '#required': true
     '#file_extensions': txt
+  files:
+    '#title': files
+    '#type': managed_file
+    '#required': true
+    '#multiple': true
+    '#file_extensions': txt
+  
 css: ''
 javascript: ''
 settings:
@@ -26,6 +35,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -33,6 +43,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: true
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -48,22 +59,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -74,6 +100,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: all
   draft_multiple: false
   draft_auto_save: false
@@ -83,6 +110,7 @@ settings:
   confirmation_title: ''
   confirmation_message: |
     <p>Your confirmation number is [webform_submission:values:confirmation_number].</p>
+    
   confirmation_url: ''
   confirmation_attributes: {  }
   confirmation_back: true
@@ -93,9 +121,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -148,6 +178,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   remote_post:
     id: remote_post
@@ -194,4 +228,4 @@ handlers:
       converted_url: 'http://webform-test-handler-remote-post/converted'
       converted_custom_data: ''
       message: ''
-      messages: { }
+      messages: {  }
diff --git a/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_put.yml b/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_put.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cec9caef839ea338636a43134e248b9b160707d9
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_handler_remote_post/config/install/webform.webform.test_handler_remote_put.yml
@@ -0,0 +1,257 @@
+langcode: en
+status: open
+dependencies:
+  enforced:
+    module:
+      - webform_test_handler_remote_post
+open: null
+close: null
+weight: 0
+uid: null
+template: false
+archive: false
+id: test_handler_remote_put
+title: 'Test: Handler: Remote HTTP PUT'
+description: 'Test remote post handler using HTTP PUT method.'
+category: 'Test: Handler'
+elements: |
+  response_type:
+    '#title': 'Response type'
+    '#type': select
+    '#options':
+      200: '200 OK'
+      500: '500 Internal Server Error'
+      404: '404 Not Found'
+    '#default_value': 200
+  first_name:
+    '#title': 'First name'
+    '#type': textfield
+    '#required': true
+    '#default_value': John
+  last_name:
+    '#title': 'Last name'
+    '#type': textfield
+    '#required': true
+    '#default_value': Smith
+  confirmation_number:
+    '#title': 'Confirmation number'
+    '#type': value
+    '#value': '[webform:handler:remote_post:completed:confirmation_number]'
+  
+css: ''
+javascript: ''
+settings:
+  ajax: false
+  ajax_scroll_top: form
+  page: true
+  page_submit_path: ''
+  page_confirm_path: ''
+  form_title: source_entity_webform
+  form_submit_once: false
+  form_exception_message: ''
+  form_open_message: ''
+  form_close_message: ''
+  form_previous_submissions: true
+  form_confidential: false
+  form_confidential_message: ''
+  form_remote_addr: true
+  form_convert_anonymous: true
+  form_prepopulate: false
+  form_prepopulate_source_entity: false
+  form_prepopulate_source_entity_required: false
+  form_prepopulate_source_entity_type: ''
+  form_reset: false
+  form_disable_autocomplete: false
+  form_novalidate: false
+  form_disable_inline_errors: false
+  form_required: false
+  form_unsaved: false
+  form_disable_back: false
+  form_submit_back: false
+  form_autofocus: false
+  form_details_toggle: false
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
+  submission_label: ''
+  submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
+  submission_user_columns: {  }
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
+  submission_exception_message: ''
+  submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
+  autofill: false
+  autofill_message: ''
+  autofill_excluded_elements: {  }
+  wizard_progress_bar: true
+  wizard_progress_pages: false
+  wizard_progress_percentage: false
+  wizard_progress_link: false
+  wizard_start_label: ''
+  wizard_preview_link: false
+  wizard_confirmation: true
+  wizard_confirmation_label: ''
+  wizard_track: ''
+  preview: 0
+  preview_label: ''
+  preview_title: ''
+  preview_message: ''
+  preview_attributes: {  }
+  preview_excluded_elements: {  }
+  preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
+  draft: all
+  draft_multiple: false
+  draft_auto_save: false
+  draft_saved_message: ''
+  draft_loaded_message: ''
+  confirmation_type: inline
+  confirmation_title: ''
+  confirmation_message: |
+    <p>Your confirmation number is [webform_submission:values:confirmation_number].</p>
+    
+  confirmation_url: ''
+  confirmation_attributes: {  }
+  confirmation_back: true
+  confirmation_back_label: ''
+  confirmation_back_attributes: {  }
+  confirmation_exclude_query: false
+  confirmation_exclude_token: false
+  limit_total: null
+  limit_total_interval: null
+  limit_total_message: ''
+  limit_total_unique: false
+  limit_user: null
+  limit_user_interval: null
+  limit_user_message: ''
+  limit_user_unique: false
+  entity_limit_total: null
+  entity_limit_total_interval: null
+  entity_limit_user: null
+  entity_limit_user_interval: null
+  purge: none
+  purge_days: null
+  results_disabled: false
+  results_disabled_ignore: false
+  token_update: false
+access:
+  create:
+    roles:
+      - anonymous
+      - authenticated
+    users: {  }
+    permissions: {  }
+  view_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  purge_any:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  view_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  update_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  delete_own:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  administer:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  test:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
+handlers:
+  remote_post:
+    id: remote_post
+    label: 'Remote HTTP PUT'
+    handler_id: remote_post
+    status: true
+    conditions: {  }
+    weight: 1
+    settings:
+      method: PUT
+      type: ''
+      excluded_data:
+        serial: serial
+        sid: sid
+        uuid: uuid
+        token: token
+        uri: uri
+        created: created
+        completed: completed
+        changed: changed
+        in_draft: in_draft
+        current_page: current_page
+        remote_addr: remote_addr
+        uid: uid
+        langcode: langcode
+        webform_id: webform_id
+        entity_type: entity_type
+        entity_id: entity_id
+        sticky: sticky
+        locked: locked
+        notes: notes
+        confirmation_number: confirmation_number
+      custom_data: |
+        custom_data: true
+        
+      custom_options: |
+        headers:
+          custom_header: 'true'
+        
+      debug: true
+      completed_url: 'http://webform-test-handler-remote-post/completed'
+      completed_custom_data: |
+        custom_completed: true
+        
+      updated_url: 'http://webform-test-handler-remote-post/updated'
+      updated_custom_data: |
+        custom_updated: true
+        
+      deleted_url: 'http://webform-test-handler-remote-post/deleted'
+      deleted_custom_data: |
+        custom_deleted: true
+        
+      draft_url: 'http://webform-test-handler-remote-post/draft'
+      draft_custom_data: |
+        custom_draft: true
+        
+      converted_url: 'http://webform-test-handler-remote-post/converted'
+      converted_custom_data: |
+        custom_converted: true
+        
+      message: ''
+      messages: {  }
diff --git a/web/modules/webform/tests/modules/webform_test_handler_remote_post/src/WebformTestHandlerRemotePostClient.php b/web/modules/webform/tests/modules/webform_test_handler_remote_post/src/WebformTestHandlerRemotePostClient.php
index d54a7ad5d8952b66cfd0d77f42183481355c4b67..ace37b4ec82680e8e96811acdca155d413d09831 100644
--- a/web/modules/webform/tests/modules/webform_test_handler_remote_post/src/WebformTestHandlerRemotePostClient.php
+++ b/web/modules/webform/tests/modules/webform_test_handler_remote_post/src/WebformTestHandlerRemotePostClient.php
@@ -29,7 +29,7 @@ public function request($method, $uri = '', array $options = []) {
     }
 
     $response_type = (isset($params['response_type'])) ? $params['response_type'] : 200;
-    $operation = str_replace('http://webform-test-handler-remote-post/', '', $uri);
+    $operation = ltrim(parse_url($uri, PHP_URL_PATH), '/');
     $random = new Random();
     // Handle 404 errors.
     switch ($response_type) {
@@ -42,6 +42,7 @@ public function request($method, $uri = '', array $options = []) {
         $status = 401;
         $headers = ['Content-Type' => ['application/json']];
         $json = [
+          'method' => $method,
           'status' => 'unauthorized',
           'message' => (string) new FormattableMarkup('Unauthorized to process @type request.', ['@type' => $operation]),
           'options' => $options,
@@ -53,6 +54,7 @@ public function request($method, $uri = '', array $options = []) {
         $status = 500;
         $headers = ['Content-Type' => ['application/json']];
         $json = [
+          'method' => $method,
           'status' => 'fail',
           'message' => (string) new FormattableMarkup('Failed to process @type request.', ['@type' => $operation]),
           'options' => $options,
@@ -65,6 +67,7 @@ public function request($method, $uri = '', array $options = []) {
         $status = 200;
         $headers = ['Content-Type' => ['application/json']];
         $json = [
+          'method' => $method,
           'status' => 'success',
           'message' => (string) new FormattableMarkup('Processed @type request.', ['@type' => $operation]),
           'options' => $options,
diff --git a/web/modules/webform/tests/modules/webform_test_handler_remote_post/webform_test_handler_remote_post.info.yml b/web/modules/webform/tests/modules/webform_test_handler_remote_post/webform_test_handler_remote_post.info.yml
index 6cfb6200e9e4e8a135d2999a4dc2fcfdc2acdd07..d811a4d727e14a96936456555f8c02d9d621417b 100644
--- a/web/modules/webform/tests/modules/webform_test_handler_remote_post/webform_test_handler_remote_post.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_handler_remote_post/webform_test_handler_remote_post.info.yml
@@ -6,8 +6,8 @@ package: Testing
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_markup/webform_test_markup.info.yml b/web/modules/webform/tests/modules/webform_test_markup/webform_test_markup.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a0d294aa1c8036379109e7785fe4351c82ae8ea9
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_markup/webform_test_markup.info.yml
@@ -0,0 +1,13 @@
+name: 'Webform module markup tests'
+type: module
+description: 'Support module for markup tests.'
+package: Testing
+# core: 8.x
+dependencies:
+  - 'webform:webform'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_markup/webform_test_markup.module b/web/modules/webform/tests/modules/webform_test_markup/webform_test_markup.module
new file mode 100644
index 0000000000000000000000000000000000000000..0838c16c5feec6455f60e9ae947c0fba9a52698e
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_markup/webform_test_markup.module
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Support module for markup tests.
+ */
+
+/**
+ * Prepares variables for Webform HTML Editor markup templates.
+ *
+ * @see webform.webform.test_element_markup.yml
+ */
+function webform_test_markup_preprocess_webform_html_editor_markup(array &$variables) {
+  if ((string) $variables['content']['#markup'] === '<p>Alter this markup.</p>') {
+    $variables['content']['#markup'] = '<p><em>Alter this markup.</em> <strong>This markup was altered.</strong></p>';
+  }
+}
diff --git a/web/modules/webform/tests/modules/webform_test_message_custom/webform_test_message_custom.info.yml b/web/modules/webform/tests/modules/webform_test_message_custom/webform_test_message_custom.info.yml
index 73a96b5d20d7a4f66875a652839adb37b85e8a15..586aea832af628073f84ea97245c84653f9d0f13 100644
--- a/web/modules/webform/tests/modules/webform_test_message_custom/webform_test_message_custom.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_message_custom/webform_test_message_custom.info.yml
@@ -6,8 +6,8 @@ package: Testing
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_message_custom/webform_test_message_custom.install b/web/modules/webform/tests/modules/webform_test_message_custom/webform_test_message_custom.install
index 12d8b1f547942bdaf5452ab81e595d275ee43238..b58f82ca6174f29da0edd0b0423b494a32d9afe4 100644
--- a/web/modules/webform/tests/modules/webform_test_message_custom/webform_test_message_custom.install
+++ b/web/modules/webform/tests/modules/webform_test_message_custom/webform_test_message_custom.install
@@ -5,7 +5,6 @@
  * Install, update and uninstall functions for the Webform Test Message Custom module.
  */
 
-
 /**
  * Implements hook_uninstall().
  */
diff --git a/web/modules/webform/tests/modules/webform_test_options/config/install/webform.webform.test_options.yml b/web/modules/webform/tests/modules/webform_test_options/config/install/webform.webform.test_options.yml
index cf482666c77738338f891a654a8f8147958c0cbe..e5d27cade84c50963702e4706fdc6f9f529dcbb8 100644
--- a/web/modules/webform/tests/modules/webform_test_options/config/install/webform.webform.test_options.yml
+++ b/web/modules/webform/tests/modules/webform_test_options/config/install/webform.webform.test_options.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test_options
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_options
 title: 'Test: Options'
 description: 'Test webform options.'
@@ -217,6 +219,7 @@ elements: |
       '#options': range
       '#min': a
       '#max': z
+  
 css: ''
 javascript: ''
 settings:
@@ -225,6 +228,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -232,6 +236,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -247,22 +252,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -273,6 +293,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -291,9 +312,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -346,4 +369,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test_options/webform_test_options.info.yml b/web/modules/webform/tests/modules/webform_test_options/webform_test_options.info.yml
index d8b817f1111a9070faaaa4113fb7ebcd87c08b81..c2f6198f55fdc9b0aefb5e658211aecd8facd437 100644
--- a/web/modules/webform/tests/modules/webform_test_options/webform_test_options.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_options/webform_test_options.info.yml
@@ -6,8 +6,8 @@ package: Testing
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_options/webform_test_options.module b/web/modules/webform/tests/modules/webform_test_options/webform_test_options.module
index abe403c1704f83d4ff9ee01ee5e4d3a0042aafc3..e175f6410293326910960c0c64da7ea80b5dff7e 100644
--- a/web/modules/webform/tests/modules/webform_test_options/webform_test_options.module
+++ b/web/modules/webform/tests/modules/webform_test_options/webform_test_options.module
@@ -20,7 +20,7 @@ function webform_test_options_webform_options_test_alter(array &$options, array
  * Implements hook_webform_options_alter().
  */
 function webform_test_options_webform_options_alter(array &$options, array &$element, $id) {
-  if ($id == 'custom') {
+  if ($id === 'custom') {
     $options = [
       'one' => t('One'),
       'two' => t('Two'),
diff --git a/web/modules/webform/tests/modules/webform_test_paragraphs/webform_test_paragraphs.info.yml b/web/modules/webform/tests/modules/webform_test_paragraphs/webform_test_paragraphs.info.yml
index 6a6c4f9d3e09973d7b14a041f32fd3796f6b76b3..ba1df7bcada10759064ef23b51b5b68e18d75178 100644
--- a/web/modules/webform/tests/modules/webform_test_paragraphs/webform_test_paragraphs.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_paragraphs/webform_test_paragraphs.info.yml
@@ -7,8 +7,8 @@ dependencies:
   - 'paragraphs:paragraphs'
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_rest/config/install/rest.resource.entity.webform.yml b/web/modules/webform/tests/modules/webform_test_rest/config/install/rest.resource.entity.webform.yml
new file mode 100644
index 0000000000000000000000000000000000000000..280a19d359389375f52c5af3aaf26008af9c17d2
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_rest/config/install/rest.resource.entity.webform.yml
@@ -0,0 +1,19 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - serialization
+    - user
+    - webform
+id: entity.webform
+plugin_id: 'entity:webform'
+granularity: resource
+configuration:
+  methods:
+    - GET
+  formats:
+    - json
+    - hal_json
+    - xml
+  authentication:
+    - cookie
diff --git a/web/modules/webform/tests/modules/webform_test_rest/config/install/rest.resource.entity.webform_options.yml b/web/modules/webform/tests/modules/webform_test_rest/config/install/rest.resource.entity.webform_options.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2899ef3bb79eff69540f5b1e031dc02620425f0a
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_rest/config/install/rest.resource.entity.webform_options.yml
@@ -0,0 +1,19 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - serialization
+    - user
+    - webform
+id: entity.webform_options
+plugin_id: 'entity:webform_options'
+granularity: resource
+configuration:
+  methods:
+    - GET
+  formats:
+    - json
+    - hal_json
+    - xml
+  authentication:
+    - cookie
diff --git a/web/modules/webform/tests/modules/webform_test_rest/config/install/rest.resource.entity.webform_submission.yml b/web/modules/webform/tests/modules/webform_test_rest/config/install/rest.resource.entity.webform_submission.yml
new file mode 100644
index 0000000000000000000000000000000000000000..69b7ccd7adfe3f9069fbf0f80ca055f533a913ca
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_rest/config/install/rest.resource.entity.webform_submission.yml
@@ -0,0 +1,19 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - serialization
+    - user
+    - webform
+id: entity.webform_submission
+plugin_id: 'entity:webform_submission'
+granularity: resource
+configuration:
+  methods:
+    - GET
+  formats:
+    - json
+    - hal_json
+    - xml
+  authentication:
+    - cookie
diff --git a/web/modules/webform/tests/modules/webform_test_rest/webform_test_rest.info.yml b/web/modules/webform/tests/modules/webform_test_rest/webform_test_rest.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4bbc06833e7fbfc748c7d2c6c3bb1499464c21f7
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_rest/webform_test_rest.info.yml
@@ -0,0 +1,16 @@
+name: 'Webform module REST API tests'
+type: module
+description: 'Support module for Webform module REST API testing.'
+package: Testing
+# core: 8.x
+dependencies:
+  - 'drupal:hal'
+  - 'drupal:rest'
+  - 'drupal:serialization'
+  - 'webform:webform'
+
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
+core: '8.x'
+project: 'webform'
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_submissions/config/install/webform.webform.test_submissions.yml b/web/modules/webform/tests/modules/webform_test_submissions/config/install/webform.webform.test_submissions.yml
index d87d96687934d9591241580a829480f8c9b083ad..1a6a514b3fa7beb3146dfd42cc7be7e593573236 100644
--- a/web/modules/webform/tests/modules/webform_test_submissions/config/install/webform.webform.test_submissions.yml
+++ b/web/modules/webform/tests/modules/webform_test_submissions/config/install/webform.webform.test_submissions.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test_submissions
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_submissions
 title: 'Test: Submissions'
 description: 'Test webform submissions.'
@@ -56,6 +58,7 @@ elements: |
   address:
     '#type': webform_address
     '#title': Address
+  
 css: ''
 javascript: ''
 settings:
@@ -64,6 +67,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -71,6 +75,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -86,22 +91,37 @@ settings:
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -112,6 +132,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -130,9 +151,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -185,4 +208,8 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers: {  }
diff --git a/web/modules/webform/tests/modules/webform_test_submissions/webform_test_submissions.info.yml b/web/modules/webform/tests/modules/webform_test_submissions/webform_test_submissions.info.yml
index caa0728e6d0a7b3132ca163eee04801c6b9a0d3e..510f6598735109401468ff55a5bec566a8322da9 100644
--- a/web/modules/webform/tests/modules/webform_test_submissions/webform_test_submissions.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_submissions/webform_test_submissions.info.yml
@@ -7,8 +7,8 @@ dependencies:
   - 'drupal:node'
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_third_party_settings/webform_test_third_party_settings.info.yml b/web/modules/webform/tests/modules/webform_test_third_party_settings/webform_test_third_party_settings.info.yml
index bf30bfa3c19c3f5e2cd152619b67341119184143..0d3f3603e070db7996b051dcf8251dfc490668ab 100644
--- a/web/modules/webform/tests/modules/webform_test_third_party_settings/webform_test_third_party_settings.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_third_party_settings/webform_test_third_party_settings.info.yml
@@ -6,8 +6,8 @@ package: Testing
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_third_party_settings/webform_test_third_party_settings.webform.inc b/web/modules/webform/tests/modules/webform_test_third_party_settings/webform_test_third_party_settings.webform.inc
index 4be8912c1d721c560b7077031d13c422efa3e85f..47eb1a3b0a4a901e11e61891551e44d7ab9b5b54 100644
--- a/web/modules/webform/tests/modules/webform_test_third_party_settings/webform_test_third_party_settings.webform.inc
+++ b/web/modules/webform/tests/modules/webform_test_third_party_settings/webform_test_third_party_settings.webform.inc
@@ -66,6 +66,6 @@ function webform_test_third_party_settings_webform_submission_form_alter(&$form,
 
   // If a message is set, display it.
   if ($message) {
-    drupal_set_message($message);
+    \Drupal::messenger()->addStatus($message);
   }
 }
diff --git a/web/modules/webform/tests/modules/webform_test_translation/config/install/language/es/webform.settings.yml b/web/modules/webform/tests/modules/webform_test_translation/config/install/language/es/webform.settings.yml
index b195b33576c0677dae7f3e54a66377d3e2b8f701..d05e881fd36c87ef57fb862cba345c37672445aa 100644
--- a/web/modules/webform/tests/modules/webform_test_translation/config/install/language/es/webform.settings.yml
+++ b/web/modules/webform/tests/modules/webform_test_translation/config/install/language/es/webform.settings.yml
@@ -1,7 +1,7 @@
 settings:
   default_submit_button_label: Enviar
   default_preview_next_button_label: 'Vista previa'
-  default_form_close_message: 'Lo siento ... Esta forma está cerrado a nuevas presentaciones.'
+  default_form_close_message: 'Lo siento … Esta forma está cerrado a nuevas presentaciones.'
   default_form_exception_message: 'No se puede mostrar este formulario. Por favor contacte al administrador del sitio.'
   default_preview_prev_button_label: '< Anterior'
   default_preview_message: 'Por favor revise su presentación. Su presentación no está completo hasta que se pulsa el botón "Enviar"!'
@@ -16,10 +16,11 @@ mail:
   default_body: |
     Enviada [webform_submission:created]
     Presentado por:[webform_submission:user]
-
+    
     Los valores presentados son:
-
+    
     [webform_submission:values]
+    
 test:
   types: |
     checkbox:
@@ -73,13 +74,14 @@ test:
       - 'random@random.com'
     webform_email_multiple:
       - 'example@example.com, test@test.com, random@random.com'
-    webform_location:
+    webform_location_geocomplete:
       - value: 'The White House, 1600 Pennsylvania Ave NW, Washington, DC 20500, USA'
       - value: 'London SW1A 1AA, United Kingdom'
       - value: 'Moscow, Russia, 10307'
     webform_time:
       - '09:00'
       - '17:00'
+    
   names: |
     first_name:
       - 'John'
diff --git a/web/modules/webform/tests/modules/webform_test_translation/config/install/language/es/webform.webform.test_translation.yml b/web/modules/webform/tests/modules/webform_test_translation/config/install/language/es/webform.webform.test_translation.yml
index e9fde213a6f4dd461b5a6e7339a038a4a5dfeda3..73f75dbe1430d826f136bbb1baae21507e7a07b1 100644
--- a/web/modules/webform/tests/modules/webform_test_translation/config/install/language/es/webform.webform.test_translation.yml
+++ b/web/modules/webform/tests/modules/webform_test_translation/config/install/language/es/webform.webform.test_translation.yml
@@ -11,7 +11,7 @@ elements: |
       4: 'Las cuatro'
       5: Cinco
       6: Seis
-    '#other__option_label': 'Número personalizado...'
+    '#other__option_label': 'Número personalizado…'
   details:
     '#title': Detalles
   markup:
@@ -26,6 +26,14 @@ elements: |
       age:
         '#title': Edad
         '#field_suffix': 'años. antiguo'
+  address:
+    '#title': 'Dirección'
+    '#address__title': 'Dirección'
+    '#address_2__title': 'Dirección 2'
+    '#city__title': 'Ciudad / Pueblo'
+    '#state_province__title': 'Estado / Provincia'
+    '#postal_code__title': 'ZIP / Código Postal'
+    '#country__title': 'Acciones de país'
   actions:
     '#type': webform_actions
     '#title': 'Submit button(s)'
@@ -35,6 +43,7 @@ settings:
 handlers:
   email_confirmation:
     label: ' Confirmación de correo electrónico'
+weight: 0
 dependencies:
   enforced:
     module:
diff --git a/web/modules/webform/tests/modules/webform_test_translation/config/install/language/es/webform.webform_options.test_translation.yml b/web/modules/webform/tests/modules/webform_test_translation/config/install/language/es/webform.webform_options.test_translation.yml
index e3fa6e5f7a130936da0d935d3f4a64ba0255950c..28ac3d8f7cc8384601359294576ad8cade376201 100644
--- a/web/modules/webform/tests/modules/webform_test_translation/config/install/language/es/webform.webform_options.test_translation.yml
+++ b/web/modules/webform/tests/modules/webform_test_translation/config/install/language/es/webform.webform_options.test_translation.yml
@@ -3,6 +3,7 @@ options: |
   1: Uno
   2: Dos
   3: Tres
+  
 dependencies:
   enforced:
     module:
diff --git a/web/modules/webform/tests/modules/webform_test_translation/config/install/webform.webform.test_translation.yml b/web/modules/webform/tests/modules/webform_test_translation/config/install/webform.webform.test_translation.yml
index 4e5acb878ce53937327880af36fae939aff9b9cf..437d5f98ab5acbb8bc54c70fa6f2c0ac3d6b8480 100644
--- a/web/modules/webform/tests/modules/webform_test_translation/config/install/webform.webform.test_translation.yml
+++ b/web/modules/webform/tests/modules/webform_test_translation/config/install/webform.webform.test_translation.yml
@@ -6,8 +6,10 @@ dependencies:
       - webform_test_translation
 open: null
 close: null
+weight: 0
 uid: null
 template: false
+archive: false
 id: test_translation
 title: 'Test: Translations'
 description: 'Test translating a webform.'
@@ -27,7 +29,7 @@ elements: |
       4: Four
       5: Five
       6: Six
-    '#other__option_label': 'Custom number...'
+    '#other__option_label': 'Custom number…'
   details:
     '#type': details
     '#title': Details
@@ -45,14 +47,22 @@ elements: |
         '#title': 'Last name'
       age:
         '#type': number
-        '#title': 'Age'
+        '#title': Age
         '#field_suffix': ' yrs. old'
         '#min': 1
         '#max': 125
+  address:
+    '#type': webform_address
+    '#title': Address
+  token:
+    '#type': webform_computed_token
+    '#title': 'Computed (token)'
+    '#value': 'Site name: [site:name]'
   actions:
     '#type': webform_actions
     '#title': 'Submit button(s)'
     '#submit__label': 'Send message'
+  
 css: ''
 javascript: ''
 settings:
@@ -61,6 +71,7 @@ settings:
   page: true
   page_submit_path: ''
   page_confirm_path: ''
+  form_title: source_entity_webform
   form_submit_once: false
   form_exception_message: ''
   form_open_message: ''
@@ -68,6 +79,7 @@ settings:
   form_previous_submissions: true
   form_confidential: false
   form_confidential_message: ''
+  form_remote_addr: true
   form_convert_anonymous: false
   form_prepopulate: false
   form_prepopulate_source_entity: false
@@ -76,28 +88,44 @@ settings:
   form_reset: false
   form_disable_autocomplete: false
   form_novalidate: false
+  form_disable_inline_errors: false
   form_required: false
   form_unsaved: false
   form_disable_back: false
   form_submit_back: false
   form_autofocus: false
   form_details_toggle: false
-  form_login: false
-  form_login_message: ''
+  form_access_denied: default
+  form_access_denied_title: ''
+  form_access_denied_message: ''
+  form_access_denied_attributes: {  }
+  form_file_limit: ''
   submission_label: ''
   submission_log: false
+  submission_views: {  }
+  submission_views_replace: {  }
   submission_user_columns: {  }
-  submission_login: false
-  submission_login_message: ''
+  submission_user_duplicate: false
+  submission_access_denied: default
+  submission_access_denied_title: ''
+  submission_access_denied_message: ''
+  submission_access_denied_attributes: {  }
   submission_exception_message: ''
   submission_locked_message: ''
+  submission_excluded_elements: {  }
+  submission_exclude_empty: false
+  submission_exclude_empty_checkbox: false
+  previous_submission_message: ''
+  previous_submissions_message: ''
   autofill: false
   autofill_message: ''
   autofill_excluded_elements: {  }
   wizard_progress_bar: true
   wizard_progress_pages: false
   wizard_progress_percentage: false
+  wizard_progress_link: false
   wizard_start_label: ''
+  wizard_preview_link: false
   wizard_confirmation: true
   wizard_confirmation_label: ''
   wizard_track: ''
@@ -108,6 +136,7 @@ settings:
   preview_attributes: {  }
   preview_excluded_elements: {  }
   preview_exclude_empty: true
+  preview_exclude_empty_checkbox: false
   draft: none
   draft_multiple: false
   draft_auto_save: false
@@ -126,9 +155,11 @@ settings:
   limit_total: null
   limit_total_interval: null
   limit_total_message: ''
+  limit_total_unique: false
   limit_user: null
   limit_user_interval: null
   limit_user_message: ''
+  limit_user_unique: false
   entity_limit_total: null
   entity_limit_total_interval: null
   entity_limit_user: null
@@ -181,6 +212,10 @@ access:
     roles: {  }
     users: {  }
     permissions: {  }
+  configuration:
+    roles: {  }
+    users: {  }
+    permissions: {  }
 handlers:
   email_confirmation:
     id: email
@@ -192,23 +227,25 @@ handlers:
     settings:
       states:
         - completed
-      to_mail: default
+      to_mail: _default
       to_options: {  }
       cc_mail: ''
       cc_options: {  }
       bcc_mail: ''
       bcc_options: {  }
-      from_mail: default
+      from_mail: _default
       from_options: {  }
-      from_name: default
-      subject: default
-      body: default
+      from_name: _default
+      subject: _default
+      body: _default
       excluded_elements: {  }
       ignore_access: false
       exclude_empty: true
+      exclude_empty_checkbox: false
       html: true
       attachments: false
       twig: false
+      theme_name: ''
       debug: false
       reply_to: ''
       return_path: ''
diff --git a/web/modules/webform/tests/modules/webform_test_translation/webform_test_translation.info.yml b/web/modules/webform/tests/modules/webform_test_translation/webform_test_translation.info.yml
index 9c2289c82c209c358e4606c17eb64f897480d5ea..96a41398245dd4c8c482c215d3f6d40d26c08118 100644
--- a/web/modules/webform/tests/modules/webform_test_translation/webform_test_translation.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_translation/webform_test_translation.info.yml
@@ -9,8 +9,8 @@ dependencies:
   - 'drupal:locale'
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_translation/webform_test_translation.install b/web/modules/webform/tests/modules/webform_test_translation/webform_test_translation.install
new file mode 100644
index 0000000000000000000000000000000000000000..a1ba31c7bfa484b48baab194bc05d6ccbfc1561d
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_translation/webform_test_translation.install
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Webform test translation module.
+ *
+ * drush php-eval 'module_load_include('install', 'webform_test_translation');webform_test_translation_install()'; drush cr;
+ */
+
+use Drupal\Core\Serialization\Yaml;
+
+/**
+ * Implements hook_install().
+ */
+function webform_test_translation_install() {
+  // Skip simpletest runner which has no issue importing
+  // translated configuration.
+  if (isset($GLOBALS['conf']) && array_key_exists('simpletest.settings', $GLOBALS['conf'])) {
+    return;
+  }
+
+  _webform_test_translation_install_config('config');
+  _webform_test_translation_install_config('config_snapshot');
+}
+
+/**
+ * Fixes Issue #2845437: Process translation config files for custom modules.
+ *
+ * @param string $table_name
+ *   Config table name.
+ */
+function _webform_test_translation_install_config($table_name) {
+  if (!\Drupal::database()->schema()->tableExists($table_name)) {
+    return;
+  }
+
+  $query = \Drupal::database()->select($table_name, 'c')
+    ->fields('c', ['name', 'collection', 'data'])
+    ->condition('collection', 'language.es');
+  $result = $query->execute();
+  while ($record = $result->fetchAssoc()) {
+    $data = unserialize($record['data']);
+
+    $filename = drupal_get_path('module', 'webform_test_translation') . '/config/install/language/es/' . $record['name'] . '.yml';
+    if (!file_exists($filename)) {
+      continue;
+    }
+
+    $translated_data = Yaml::decode(file_get_contents($filename));
+    foreach ($translated_data as $key => $value) {
+      $data[$key] = $value;
+    }
+
+    \Drupal::database()->update($table_name)
+      ->fields(['data' => serialize($data)])
+      ->condition('collection', $record['collection'])
+      ->condition('name', $record['name'])
+      ->execute();
+  }
+}
diff --git a/web/modules/webform/tests/modules/webform_test_translation_lingotek/config/install/language/es/webform.settings.yml b/web/modules/webform/tests/modules/webform_test_translation_lingotek/config/install/language/es/webform.settings.yml
index 2b9f490db69b47621e180097b5c6664c677d7386..7d1ad30ce382be7aec5e8a3bdf960b5b4ced0f18 100644
--- a/web/modules/webform/tests/modules/webform_test_translation_lingotek/config/install/language/es/webform.settings.yml
+++ b/web/modules/webform/tests/modules/webform_test_translation_lingotek/config/install/language/es/webform.settings.yml
@@ -1,7 +1,7 @@
 settings:
   default_form_submit_label: Enviar
   default_preview_next_button_label: 'Vista previa'
-  default_form_close_message: 'Lo siento... Este formulario está cerrado a nuevas presentaciones.'
+  default_form_close_message: 'Lo siento… Este formulario está cerrado a nuevas presentaciones.'
   default_form_exception_message: 'No se puede mostrar este formulario Web. Póngase en contacto con el administrador del sitio.'
   default_preview_prev_button_label: '&lt; Anterior'
   default_preview_message: 'Por favor revise su presentación. Su solicitud no está completa hasta que usted oprima el botón &quot;Enviar&quot;!'
diff --git a/web/modules/webform/tests/modules/webform_test_translation_lingotek/config/install/language/ru/webform.settings.yml b/web/modules/webform/tests/modules/webform_test_translation_lingotek/config/install/language/ru/webform.settings.yml
index 85f3bc298d494d7a5e8a1e6e692180e186703ce3..1e0fdaf63d66fb87e8091888152af135ff75c705 100644
--- a/web/modules/webform/tests/modules/webform_test_translation_lingotek/config/install/language/ru/webform.settings.yml
+++ b/web/modules/webform/tests/modules/webform_test_translation_lingotek/config/install/language/ru/webform.settings.yml
@@ -2,7 +2,7 @@ settings:
   default_form_submit_label: Отправить
   default_preview_next_button_label: 'Предварительный просмотр'
   default_form_open_message: 'Эта форма еще не был открыт для представлений.'
-  default_form_close_message: 'Извините... Эта форма закрыта для новых представлений.'
+  default_form_close_message: 'Извините… Эта форма закрыта для новых представлений.'
   default_form_exception_message: 'Не удается отобразить эту веб-форму. Пожалуйста, свяжитесь с администратором сайта.'
   default_form_confidential_message: 'Эта форма является конфиденциальной. Вы должны <a href="[site:login-url]/logout?destination=[current-page:url:relative]">выйти из</a> представить его.'
   default_wizard_prev_button_label: '&lt; Предыдущая страница'
diff --git a/web/modules/webform/tests/modules/webform_test_translation_lingotek/webform_test_translation_lingotek.info.yml b/web/modules/webform/tests/modules/webform_test_translation_lingotek/webform_test_translation_lingotek.info.yml
index de1c92ea2efea3d010a806005c845824462dbdf7..e20e97878451726b554f48aacc45e92c076b8ed4 100644
--- a/web/modules/webform/tests/modules/webform_test_translation_lingotek/webform_test_translation_lingotek.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_translation_lingotek/webform_test_translation_lingotek.info.yml
@@ -10,8 +10,8 @@ dependencies:
   - 'lingotek:lingotek'
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_validate/webform_test_validate.info.yml b/web/modules/webform/tests/modules/webform_test_validate/webform_test_validate.info.yml
index 09e2b947881ae54d046ea72cdfb92606219130c4..ef28015ba4dd8cc95242b0bb61d163ec8e95ee41 100644
--- a/web/modules/webform/tests/modules/webform_test_validate/webform_test_validate.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_validate/webform_test_validate.info.yml
@@ -6,8 +6,8 @@ package: Testing
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_views/config/install/views.view.webform_test_views_access.yml b/web/modules/webform/tests/modules/webform_test_views/config/install/views.view.webform_test_views_access.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5cbf535aea34c541cd9817a53c0f1fc740db1653
--- /dev/null
+++ b/web/modules/webform/tests/modules/webform_test_views/config/install/views.view.webform_test_views_access.yml
@@ -0,0 +1,298 @@
+langcode: en
+status: true
+dependencies:
+  enforced:
+    module:
+      - webform_test_views
+  module:
+    - user
+    - webform
+id: webform_test_views_access
+label: 'Webform test: Views access'
+module: views
+description: 'Test webform submission access.'
+tag: ''
+base_table: webform_submission
+base_field: sid
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: perm
+        options:
+          perm: 'access webform overview'
+      cache:
+        type: tag
+        options: {  }
+      query:
+        type: views_query
+        options:
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_comment: ''
+          query_tags: {  }
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      pager:
+        type: full
+        options:
+          items_per_page: 50
+          offset: 0
+          id: 0
+          total_pages: null
+          tags:
+            previous: ‹‹
+            next: ››
+            first: '« First'
+            last: 'Last »'
+          expose:
+            items_per_page: true
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50, 100, 200'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+          quantity: 9
+      style:
+        type: table
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          override: true
+          sticky: false
+          caption: ''
+          summary: ''
+          description: ''
+          columns:
+            sid: sid
+            webform_id: webform_id
+          info:
+            sid:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            webform_id:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+          default: sid
+          empty_table: false
+      row:
+        type: fields
+        options:
+          inline: {  }
+          separator: ''
+          hide_empty: false
+          default_field_elements: true
+      fields:
+        sid:
+          id: sid
+          table: webform_submission
+          field: sid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: '#'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: number_integer
+          settings:
+            thousand_separator: ''
+            prefix_suffix: true
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: sid
+          plugin_id: field
+        webform_id:
+          id: webform_id
+          table: webform_submission
+          field: webform_id
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Webform
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: target_id
+          type: entity_reference_label
+          settings:
+            link: true
+          group_column: target_id
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: webform_submission
+          entity_field: webform_id
+          plugin_id: field
+      filters: {  }
+      sorts: {  }
+      header: {  }
+      footer: {  }
+      empty:
+        area_text_custom:
+          id: area_text_custom
+          table: views
+          field: area_text_custom
+          relationship: none
+          group_type: group
+          admin_label: ''
+          empty: true
+          tokenize: false
+          content: 'No submissions available.'
+          plugin_id: text_custom
+      relationships: {  }
+      arguments: {  }
+      display_extenders: {  }
+      filter_groups:
+        operator: AND
+        groups: {  }
+      title: 'Views bulk form'
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+        - user
+        - user.permissions
+      tags: {  }
+  page:
+    display_plugin: page
+    id: page
+    display_title: Page
+    position: 1
+    display_options:
+      display_extenders: {  }
+      path: admin/structure/webform/test/views_access
+      menu:
+        type: normal
+        title: 'Views access'
+        description: 'Test webform submission access.'
+        expanded: false
+        parent: webform_test.index
+        weight: 0
+        context: '0'
+        menu_name: admin
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+        - user
+        - user.permissions
+      tags: {  }
diff --git a/web/modules/webform/tests/modules/webform_test_views/config/install/views.view.webform_test_views_bulk_form.yml b/web/modules/webform/tests/modules/webform_test_views/config/install/views.view.webform_test_views_bulk_form.yml
index 358ce902a79a70932ec6c8258688dc0330092761..994c565724c2077a6ceaeb3d00e3901dcb762090 100644
--- a/web/modules/webform/tests/modules/webform_test_views/config/install/views.view.webform_test_views_bulk_form.yml
+++ b/web/modules/webform/tests/modules/webform_test_views/config/install/views.view.webform_test_views_bulk_form.yml
@@ -943,7 +943,35 @@ display:
           content: 'No submissions available.'
           plugin_id: text_custom
       relationships: {  }
-      arguments: {  }
+      arguments:
+        webform_id:
+          id: webform_id
+          table: webform_submission
+          field: webform_id
+          entity_type: webform_submission
+          entity_field: webform_id
+          plugin_id: string
+        entity_type:
+          id: entity_type
+          table: webform_submission
+          field: entity_type
+          entity_type: webform_submission
+          entity_field: entity_type
+          plugin_id: string
+        entity_id:
+          id: entity_id
+          table: webform_submission
+          field: entity_id
+          entity_type: webform_submission
+          entity_field: entity_id
+          plugin_id: string
+        uid:
+          id: uid
+          table: webform_submission
+          field: uid
+          entity_type: webform_submission
+          entity_field: uid
+          plugin_id: numeric
       display_extenders: {  }
       filter_groups:
         operator: AND
@@ -960,6 +988,23 @@ display:
         - user
         - user.permissions
       tags: {  }
+  embed:
+    display_plugin: embed
+    id: embed
+    display_title: Embed
+    position: 2
+    display_options:
+      display_extenders: {  }
+    cache_metadata:
+      max-age: 0
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - user
+        - user.permissions
+      tags: {  }
   page:
     display_plugin: page
     id: page
diff --git a/web/modules/webform/tests/modules/webform_test_views/webform_test_views.info.yml b/web/modules/webform/tests/modules/webform_test_views/webform_test_views.info.yml
index 83dd47c58896ff755ae7c38fd7054951391534e7..06cd56b330447981bd4b5f6c919024aab076bf02 100644
--- a/web/modules/webform/tests/modules/webform_test_views/webform_test_views.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_views/webform_test_views.info.yml
@@ -9,8 +9,8 @@ dependencies:
   - 'drupal:views'
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/modules/webform_test_wizard_custom/webform_test_wizard_custom.info.yml b/web/modules/webform/tests/modules/webform_test_wizard_custom/webform_test_wizard_custom.info.yml
index 9cb58fb81083054302c7c0f39e516660ebe11ea6..8af24c8e3b297cbee03bf05ed2c9068bed502c93 100644
--- a/web/modules/webform/tests/modules/webform_test_wizard_custom/webform_test_wizard_custom.info.yml
+++ b/web/modules/webform/tests/modules/webform_test_wizard_custom/webform_test_wizard_custom.info.yml
@@ -6,8 +6,8 @@ package: Testing
 dependencies:
   - 'webform:webform'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/tests/src/Functional/WebformAssertLegacyTrait.php b/web/modules/webform/tests/src/Functional/WebformAssertLegacyTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..f13a611985a1e5d96bf01324c15831412d8d78c9
--- /dev/null
+++ b/web/modules/webform/tests/src/Functional/WebformAssertLegacyTrait.php
@@ -0,0 +1,768 @@
+<?php
+
+namespace Drupal\Tests\webform\Functional;
+
+use Behat\Mink\Element\NodeElement;
+use Behat\Mink\Exception\ExpectationException;
+use Behat\Mink\Selector\Xpath\Escaper;
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Component\Utility\Xss;
+
+/**
+ * Provides convenience methods for assertions in browser tests.
+ *
+ * Copies of legacy traits without deprecated warnings.
+ *
+ * @see \Drupal\KernelTests\AssertLegacyTrait
+ * @see \Drupal\FunctionalTests\AssertLegacyTrait
+ * @see http://blog.fclement.info/convert-simpletest-to-phpunit
+ * @see https://www.drupal.org/node/2735005
+ */
+trait WebformAssertLegacyTrait {
+
+  /**
+   * @see \Drupal\simpletest\TestBase::assert()
+   */
+  protected function assert($actual, $message = '') {
+    parent::assertTrue((bool) $actual, $message);
+  }
+
+  /**
+   * @see \Drupal\simpletest\TestBase::assertTrue()
+   */
+  public static function assertTrue($actual, $message = '') {
+    if (is_bool($actual)) {
+      parent::assertTrue($actual, $message);
+    }
+    else {
+      parent::assertNotEmpty($actual, $message);
+    }
+  }
+
+  /**
+   * @see \Drupal\simpletest\TestBase::assertFalse()
+   */
+  public static function assertFalse($actual, $message = '') {
+    if (is_bool($actual)) {
+      parent::assertFalse($actual, $message);
+    }
+    else {
+      parent::assertEmpty($actual, $message);
+    }
+  }
+
+  /**
+   * @see \Drupal\simpletest\TestBase::assertEqual()
+   */
+  protected function assertEqual($actual, $expected, $message = '') {
+    $this->assertEquals($expected, $actual, $message);
+  }
+
+  /**
+   * @see \Drupal\simpletest\TestBase::assertNotEqual()
+   */
+  protected function assertNotEqual($actual, $expected, $message = '') {
+    $this->assertNotEquals($expected, $actual, $message);
+  }
+
+  /**
+   * @see \Drupal\simpletest\TestBase::assertIdentical()
+   */
+  protected function assertIdentical($actual, $expected, $message = '') {
+    $this->assertSame($expected, $actual, $message);
+  }
+
+  /**
+   * @see \Drupal\simpletest\TestBase::assertNotIdentical()
+   */
+  protected function assertNotIdentical($actual, $expected, $message = '') {
+    $this->assertNotSame($expected, $actual, $message);
+  }
+
+  /**
+   * @see \Drupal\simpletest\TestBase::assertIdenticalObject()
+   */
+  protected function assertIdenticalObject($actual, $expected, $message = '') {
+    // Note: ::assertSame checks whether its the same object. ::assertEquals
+    // though compares
+    $this->assertEquals($expected, $actual, $message);
+  }
+
+  /**
+   * @see \Drupal\simpletest\TestBase::pass()
+   */
+  protected function pass($message) {
+    $this->assertTrue(TRUE, $message);
+  }
+
+  /**
+   * @see \Drupal\simpletest\TestBase::verbose()
+   */
+  protected function verbose($message) {
+    if (in_array('--debug', $_SERVER['argv'], TRUE)) {
+      // Write directly to STDOUT to not produce unexpected test output.
+      // The STDOUT stream does not obey output buffering.
+      fwrite(STDOUT, $message . "\n");
+    }
+  }
+
+  /**
+   * Asserts that the element with the given CSS selector is present.
+   *
+   * @param string $css_selector
+   *   The CSS selector identifying the element to check.
+   */
+  protected function assertElementPresent($css_selector) {
+    $this->assertSession()->elementExists('css', $css_selector);
+  }
+
+  /**
+   * Asserts that the element with the given CSS selector is not present.
+   *
+   * @param string $css_selector
+   *   The CSS selector identifying the element to check.
+   */
+  protected function assertElementNotPresent($css_selector) {
+    $this->assertSession()->elementNotExists('css', $css_selector);
+  }
+
+  /**
+   * Passes if the page (with HTML stripped) contains the text.
+   *
+   * Note that stripping HTML tags also removes their attributes, such as
+   * the values of text fields.
+   *
+   * @param string $text
+   *   Plain text to look for.
+   */
+  protected function assertText($text) {
+    // Cast MarkupInterface to string.
+    $text = (string) $text;
+
+    $content_type = $this->getSession()->getResponseHeader('Content-type');
+    // In case of a Non-HTML response (example: XML) check the original
+    // response.
+    if (strpos($content_type, 'html') === FALSE) {
+      $this->assertSession()->responseContains($text);
+    }
+    else {
+      $this->assertTextHelper($text, FALSE);
+    }
+  }
+
+  /**
+   * Passes if the page (with HTML stripped) does not contains the text.
+   *
+   * Note that stripping HTML tags also removes their attributes, such as
+   * the values of text fields.
+   *
+   * @param string $text
+   *   Plain text to look for.
+   */
+  protected function assertNoText($text) {
+    // Cast MarkupInterface to string.
+    $text = (string) $text;
+
+    $content_type = $this->getSession()->getResponseHeader('Content-type');
+    // In case of a Non-HTML response (example: XML) check the original
+    // response.
+    if (strpos($content_type, 'html') === FALSE) {
+      $this->assertSession()->responseNotContains($text);
+    }
+    else {
+      $this->assertTextHelper($text);
+    }
+  }
+
+  /**
+   * Helper for assertText and assertNoText.
+   *
+   * @param string $text
+   *   Plain text to look for.
+   * @param bool $not_exists
+   *   (optional) TRUE if this text should not exist, FALSE if it should.
+   *   Defaults to TRUE.
+   *
+   * @return bool
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertTextHelper($text, $not_exists = TRUE) {
+    $args = ['@text' => $text];
+    $message = $not_exists ? new FormattableMarkup('"@text" not found', $args) : new FormattableMarkup('"@text" found', $args);
+
+    $raw_content = $this->getSession()->getPage()->getContent();
+    // Trying to simulate what the user sees, given that it removes all text
+    // inside the head tags, removes inline Javascript, fix all HTML entities,
+    // removes dangerous protocols and filtering out all HTML tags, as they are
+    // not visible in a normal browser.
+    $raw_content = preg_replace('@<head>(.+?)</head>@si', '', $raw_content);
+    $page_text = Xss::filter($raw_content, []);
+
+    $actual = $not_exists == (strpos($page_text, (string) $text) === FALSE);
+    $this->assertTrue($actual, $message);
+
+    return $actual;
+  }
+
+  /**
+   * Passes if the text is found ONLY ONCE on the text version of the page.
+   *
+   * The text version is the equivalent of what a user would see when viewing
+   * through a web browser. In other words the HTML has been filtered out of
+   * the contents.
+   *
+   * @param string|\Drupal\Component\Render\MarkupInterface $text
+   *   Plain text to look for.
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages with t(). If left blank, a default message will be displayed.
+   */
+  protected function assertUniqueText($text, $message = NULL) {
+    // Cast MarkupInterface objects to string.
+    $text = (string) $text;
+
+    $message = $message ?: "'$text' found only once on the page";
+    $page_text = $this->getSession()->getPage()->getText();
+    $nr_found = substr_count($page_text, $text);
+    $this->assertSame(1, $nr_found, $message);
+  }
+
+  /**
+   * Passes if the text is found MORE THAN ONCE on the text version of the page.
+   *
+   * The text version is the equivalent of what a user would see when viewing
+   * through a web browser. In other words the HTML has been filtered out of
+   * the contents.
+   *
+   * @param string|\Drupal\Component\Render\MarkupInterface $text
+   *   Plain text to look for.
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages with t(). If left blank, a default message will be displayed.
+   */
+  protected function assertNoUniqueText($text, $message = '') {
+    // Cast MarkupInterface objects to string.
+    $text = (string) $text;
+
+    $message = $message ?: "'$text' found more than once on the page";
+    $page_text = $this->getSession()->getPage()->getText();
+    $nr_found = substr_count($page_text, $text);
+    $this->assertGreaterThan(1, $nr_found, $message);
+  }
+
+  /**
+   * Asserts the page responds with the specified response code.
+   *
+   * @param int $code
+   *   Response code. For example 200 is a successful page request. For a list
+   *   of all codes see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
+   */
+  protected function assertResponse($code) {
+    $this->assertSession()->statusCodeEquals($code);
+  }
+
+  /**
+   * Asserts that a field exists with the given name and value.
+   *
+   * @param string $name
+   *   Name of field to assert.
+   * @param string $value
+   *   (optional) Value of the field to assert. You may pass in NULL (default)
+   *   to skip checking the actual value, while still checking that the field
+   *   exists.
+   */
+  protected function assertFieldByName($name, $value = NULL) {
+    $this->assertFieldByXPath($this->constructFieldXpath('name', $name), $value);
+  }
+
+  /**
+   * Asserts that a field does not exist with the given name and value.
+   *
+   * @param string $name
+   *   Name of field to assert.
+   * @param string $value
+   *   (optional) Value for the field, to assert that the field's value on the
+   *   page does not match it. You may pass in NULL to skip checking the
+   *   value, while still checking that the field does not exist. However, the
+   *   default value ('') asserts that the field value is not an empty string.
+   */
+  protected function assertNoFieldByName($name, $value = '') {
+    $this->assertNoFieldByXPath($this->constructFieldXpath('name', $name), $value);
+  }
+
+  /**
+   * Asserts that a field exists with the given ID and value.
+   *
+   * @param string $id
+   *   ID of field to assert.
+   * @param string|\Drupal\Component\Render\MarkupInterface $value
+   *   (optional) Value for the field to assert. You may pass in NULL to skip
+   *   checking the value, while still checking that the field exists.
+   *   However, the default value ('') asserts that the field value is an empty
+   *   string.
+   *
+   * @throws \Behat\Mink\Exception\ElementNotFoundException
+   */
+  protected function assertFieldById($id, $value = '') {
+    $this->assertFieldByXPath($this->constructFieldXpath('id', $id), $value);
+  }
+
+  /**
+   * Asserts that a field exists with the given name or ID.
+   *
+   * @param string $field
+   *   Name or ID of field to assert.
+   */
+  protected function assertField($field) {
+    $this->assertFieldByXPath($this->constructFieldXpath('name', $field) . '|' . $this->constructFieldXpath('id', $field));
+  }
+
+  /**
+   * Asserts that a field does NOT exist with the given name or ID.
+   *
+   * @param string $field
+   *   Name or ID of field to assert.
+   */
+  protected function assertNoField($field) {
+    $this->assertNoFieldByXPath($this->constructFieldXpath('name', $field) . '|' . $this->constructFieldXpath('id', $field));
+  }
+
+  /**
+   * Passes if the raw text IS found on the loaded page, fail otherwise.
+   *
+   * Raw text refers to the raw HTML that the page generated.
+   *
+   * @param string $raw
+   *   Raw (HTML) string to look for.
+   */
+  protected function assertRaw($raw) {
+    $this->assertSession()->responseContains($raw);
+  }
+
+  /**
+   * Passes if the raw text IS not found on the loaded page, fail otherwise.
+   *
+   * Raw text refers to the raw HTML that the page generated.
+   *
+   * @param string $raw
+   *   Raw (HTML) string to look for.
+   */
+  protected function assertNoRaw($raw) {
+    $this->assertSession()->responseNotContains($raw);
+  }
+
+  /**
+   * Pass if the page title is the given string.
+   *
+   * @param string $expected_title
+   *   The string the page title should be.
+   */
+  protected function assertTitle($expected_title) {
+    // Cast MarkupInterface to string.
+    $expected_title = (string) $expected_title;
+    return $this->assertSession()->titleEquals($expected_title);
+  }
+
+  /**
+   * Passes if a link with the specified label is found.
+   *
+   * An optional link index may be passed.
+   *
+   * @param string|\Drupal\Component\Render\MarkupInterface $label
+   *   Text between the anchor tags.
+   * @param int $index
+   *   Link position counting from zero.
+   */
+  protected function assertLink($label, $index = 0) {
+    return $this->assertSession()->linkExists($label, $index);
+  }
+
+  /**
+   * Passes if a link with the specified label is not found.
+   *
+   * @param string|\Drupal\Component\Render\MarkupInterface $label
+   *   Text between the anchor tags.
+   */
+  protected function assertNoLink($label) {
+    return $this->assertSession()->linkNotExists($label);
+  }
+
+  /**
+   * Passes if a link containing a given href (part) is found.
+   *
+   * @param string $href
+   *   The full or partial value of the 'href' attribute of the anchor tag.
+   * @param int $index
+   *   Link position counting from zero.
+   */
+  protected function assertLinkByHref($href, $index = 0) {
+    $this->assertSession()->linkByHrefExists($href, $index);
+  }
+
+  /**
+   * Passes if a link containing a given href (part) is not found.
+   *
+   * @param string $href
+   *   The full or partial value of the 'href' attribute of the anchor tag.
+   */
+  protected function assertNoLinkByHref($href) {
+    $this->assertSession()->linkByHrefNotExists($href);
+  }
+
+  /**
+   * Asserts that a field does not exist with the given ID and value.
+   *
+   * @param string $id
+   *   ID of field to assert.
+   * @param string $value
+   *   (optional) Value for the field, to assert that the field's value on the
+   *   page doesn't match it. You may pass in NULL to skip checking the value,
+   *   while still checking that the field doesn't exist. However, the default
+   *   value ('') asserts that the field value is not an empty string.
+   *
+   * @throws \Behat\Mink\Exception\ExpectationException
+   */
+  protected function assertNoFieldById($id, $value = '') {
+    $this->assertNoFieldByXPath($this->constructFieldXpath('id', $id), $value);
+  }
+
+  /**
+   * Passes if the internal browser's URL matches the given path.
+   *
+   * @param \Drupal\Core\Url|string $path
+   *   The expected system path or URL.
+   */
+  protected function assertUrl($path) {
+    $this->assertSession()->addressEquals($path);
+  }
+
+  /**
+   * Asserts that a select option in the current page exists.
+   *
+   * @param string $id
+   *   ID of select field to assert.
+   * @param string $option
+   *   Option to assert.
+   */
+  protected function assertOption($id, $option) {
+    return $this->assertSession()->optionExists($id, $option);
+  }
+
+  /**
+   * Asserts that a select option with the visible text exists.
+   *
+   * @param string $id
+   *   The ID of the select field to assert.
+   * @param string $text
+   *   The text for the option tag to assert.
+   */
+  protected function assertOptionByText($id, $text) {
+    return $this->assertSession()->optionExists($id, $text);
+  }
+
+  /**
+   * Asserts that a select option does NOT exist in the current page.
+   *
+   * @param string $id
+   *   ID of select field to assert.
+   * @param string $option
+   *   Option to assert.
+   */
+  protected function assertNoOption($id, $option) {
+    return $this->assertSession()->optionNotExists($id, $option);
+  }
+
+  /**
+   * Asserts that a select option in the current page is checked.
+   *
+   * @param string $id
+   *   ID of select field to assert.
+   * @param string $option
+   *   Option to assert.
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages with t(). If left blank, a default message will be displayed.
+   */
+  protected function assertOptionSelected($id, $option, $message = NULL) {
+    $option_field = $this->assertSession()->optionExists($id, $option);
+    $message = $message ?: "Option $option for field $id is selected.";
+    $this->assertTrue($option_field->hasAttribute('selected'), $message);
+  }
+
+  /**
+   * Asserts that a checkbox field in the current page is checked.
+   *
+   * @param string $id
+   *   ID of field to assert.
+   */
+  protected function assertFieldChecked($id) {
+    $this->assertSession()->checkboxChecked($id);
+  }
+
+  /**
+   * Asserts that a checkbox field in the current page is not checked.
+   *
+   * @param string $id
+   *   ID of field to assert.
+   */
+  protected function assertNoFieldChecked($id) {
+    $this->assertSession()->checkboxNotChecked($id);
+  }
+
+  /**
+   * Asserts that a field exists in the current page by the given XPath.
+   *
+   * @param string $xpath
+   *   XPath used to find the field.
+   * @param string $value
+   *   (optional) Value of the field to assert. You may pass in NULL (default)
+   *   to skip checking the actual value, while still checking that the field
+   *   exists.
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages with t().
+   */
+  protected function assertFieldByXPath($xpath, $value = NULL, $message = '') {
+    $fields = $this->xpath($xpath);
+
+    $this->assertFieldsByValue($fields, $value, $message);
+  }
+
+  /**
+   * Asserts that a field does not exist or its value does not match, by XPath.
+   *
+   * @param string $xpath
+   *   XPath used to find the field.
+   * @param string $value
+   *   (optional) Value of the field, to assert that the field's value on the
+   *   page does not match it.
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages with t().
+   *
+   * @throws \Behat\Mink\Exception\ExpectationException
+   */
+  protected function assertNoFieldByXPath($xpath, $value = NULL, $message = '') {
+    $fields = $this->xpath($xpath);
+
+    if (!empty($fields)) {
+      if (isset($value)) {
+        $found = FALSE;
+        try {
+          $this->assertFieldsByValue($fields, $value);
+          $found = TRUE;
+        }
+        catch (\Exception $e) {
+        }
+
+        if ($found) {
+          throw new ExpectationException(sprintf('The field resulting from %s was found with the provided value %s.', $xpath, $value), $this->getSession()->getDriver());
+        }
+      }
+      else {
+        throw new ExpectationException(sprintf('The field resulting from %s was found.', $xpath), $this->getSession()->getDriver());
+      }
+    }
+  }
+
+  /**
+   * Asserts that a field exists in the current page with a given Xpath result.
+   *
+   * @param \Behat\Mink\Element\NodeElement[] $fields
+   *   Xml elements.
+   * @param string $value
+   *   (optional) Value of the field to assert. You may pass in NULL (default) to skip
+   *   checking the actual value, while still checking that the field exists.
+   * @param string $message
+   *   (optional) A message to display with the assertion. Do not translate
+   *   messages with t().
+   */
+  protected function assertFieldsByValue($fields, $value = NULL, $message = '') {
+    // If value specified then check array for match.
+    $found = TRUE;
+    if (isset($value)) {
+      $found = FALSE;
+      if ($fields) {
+        foreach ($fields as $field) {
+          if ($field->getAttribute('type') == 'checkbox') {
+            if (is_bool($value)) {
+              $found = $field->isChecked() == $value;
+            }
+            else {
+              $found = TRUE;
+            }
+          }
+          elseif ($field->getAttribute('value') == $value) {
+            // Input element with correct value.
+            $found = TRUE;
+          }
+          elseif ($field->find('xpath', '//option[@value = ' . (new Escaper())->escapeLiteral($value) . ' and @selected = "selected"]')) {
+            // Select element with an option.
+            $found = TRUE;
+          }
+          elseif ($field->getTagName() === 'textarea' && $field->getValue() == $value) {
+            // Text area with correct text. Use getValue() here because
+            // getText() would remove any newlines in the value.
+            $found = TRUE;
+          }
+          elseif ($field->getTagName() !== 'input' && $field->getText() == $value) {
+            $found = TRUE;
+          }
+        }
+      }
+    }
+    $this->assertTrue($fields && $found, $message);
+  }
+
+  /**
+   * Passes if the raw text IS found escaped on the loaded page, fail otherwise.
+   *
+   * Raw text refers to the raw HTML that the page generated.
+   *
+   * @param string $raw
+   *   Raw (HTML) string to look for.
+   */
+  protected function assertEscaped($raw) {
+    $this->assertSession()->assertEscaped($raw);
+  }
+
+  /**
+   * Passes if the raw text is not found escaped on the loaded page.
+   *
+   * Raw text refers to the raw HTML that the page generated.
+   *
+   * @param string $raw
+   *   Raw (HTML) string to look for.
+   */
+  protected function assertNoEscaped($raw) {
+    $this->assertSession()->assertNoEscaped($raw);
+  }
+
+  /**
+   * Triggers a pass if the Perl regex pattern is found in the raw content.
+   *
+   * @param string $pattern
+   *   Perl regex to look for including the regex delimiters.
+   */
+  protected function assertPattern($pattern) {
+    $this->assertSession()->responseMatches($pattern);
+  }
+
+  /**
+   * Triggers a pass if the Perl regex pattern is not found in the raw content.
+   *
+   * @param string $pattern
+   *   Perl regex to look for including the regex delimiters.
+   *
+   * @see https://www.drupal.org/node/2864262
+   */
+  protected function assertNoPattern($pattern) {
+    $this->assertSession()->responseNotMatches($pattern);
+  }
+
+  /**
+   * Asserts whether an expected cache tag was present in the last response.
+   *
+   * @param string $expected_cache_tag
+   *   The expected cache tag.
+   */
+  protected function assertCacheTag($expected_cache_tag) {
+    $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', $expected_cache_tag);
+  }
+
+  /**
+   * Asserts whether an expected cache tag was absent in the last response.
+   *
+   * @param string $cache_tag
+   *   The cache tag to check.
+   *
+   * @see https://www.drupal.org/node/2864029
+   */
+  protected function assertNoCacheTag($cache_tag) {
+    $this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', $cache_tag);
+  }
+
+  /**
+   * Checks that current response header equals value.
+   *
+   * @param string $name
+   *   Name of header to assert.
+   * @param string $value
+   *   Value of the header to assert.
+   */
+  protected function assertHeader($name, $value) {
+    $this->assertSession()->responseHeaderEquals($name, $value);
+  }
+
+  /**
+   * Returns WebAssert object.
+   *
+   * @param string $name
+   *   (optional) Name of the session. Defaults to the active session.
+   *
+   * @return \Drupal\Tests\WebAssert
+   *   A new web-assert option for asserting the presence of elements with.
+   */
+  abstract public function assertSession($name = NULL);
+
+  /**
+   * Builds an XPath query.
+   *
+   * Builds an XPath query by replacing placeholders in the query by the value
+   * of the arguments.
+   *
+   * XPath 1.0 (the version supported by libxml2, the underlying XML library
+   * used by PHP) doesn't support any form of quotation. This function
+   * simplifies the building of XPath expression.
+   *
+   * @param string $xpath
+   *   An XPath query, possibly with placeholders in the form ':name'.
+   * @param array $args
+   *   An array of arguments with keys in the form ':name' matching the
+   *   placeholders in the query. The values may be either strings or numeric
+   *   values.
+   *
+   * @return string
+   *   An XPath query with arguments replaced.
+   */
+  protected function buildXPathQuery($xpath, array $args = []) {
+    return $this->assertSession()->buildXPathQuery($xpath, $args);
+  }
+
+  /**
+   * Helper: Constructs an XPath for the given set of attributes and value.
+   *
+   * @param string $attribute
+   *   Field attributes.
+   * @param string $value
+   *   Value of field.
+   *
+   * @return string
+   *   XPath for specified values.
+   */
+  protected function constructFieldXpath($attribute, $value) {
+    $xpath = '//textarea[@' . $attribute . '=:value]|//input[@' . $attribute . '=:value]|//select[@' . $attribute . '=:value]';
+    return $this->buildXPathQuery($xpath, [':value' => $value]);
+  }
+
+  /**
+   * Gets the current raw content.
+   */
+  protected function getRawContent() {
+    return $this->getSession()->getPage()->getContent();
+  }
+
+  /**
+   * Get all option elements, including nested options, in a select.
+   *
+   * @param \Behat\Mink\Element\NodeElement $element
+   *   The element for which to get the options.
+   *
+   * @return \Behat\Mink\Element\NodeElement[]
+   *   Option elements in select.
+   */
+  protected function getAllOptions(NodeElement $element) {
+    return $element->findAll('xpath', '//option');
+  }
+
+}
diff --git a/web/modules/webform/tests/src/Functional/WebformBlockCacheTest.php b/web/modules/webform/tests/src/Functional/WebformBlockCacheTest.php
index 8f996138eb59fc9ad967fa6298cfd1a88bfea3b4..75076e57849f62b4ba1cb36eb5f1e50f85dd8d2c 100644
--- a/web/modules/webform/tests/src/Functional/WebformBlockCacheTest.php
+++ b/web/modules/webform/tests/src/Functional/WebformBlockCacheTest.php
@@ -22,7 +22,7 @@ class WebformBlockCacheTest extends BrowserTestBase {
 
   /**
    * Authenticated user.
-   * 
+   *
    * @var \Drupal\user\Entity\User
    */
   private $authenticatedUser;
@@ -33,7 +33,7 @@ class WebformBlockCacheTest extends BrowserTestBase {
   protected function setUp() {
     parent::setUp();
 
-    $this->authenticatedUser = $this->drupalCreateUser([
+    $this->authenticatedUser = $this->createUser([
       'access content',
     ]);
 
@@ -78,13 +78,17 @@ public function testAuthenticatedVisitIsCacheable() {
    * Test that if an Webform is access restricted the page can still be cached.
    */
   public function testAuthenticatedAndRestrictedVisitIsCacheable() {
+    /** @var \Drupal\webform\WebformAccessRulesManagerInterface $access_rules_manager */
+    $access_rules_manager = \Drupal::service('webform.access_rules_manager');
+    $default_access_rules = $access_rules_manager->getDefaultAccessRules();
+
     $access_rules = [
       'create' => [
         'roles' => [],
         'users' => [],
         'permissions' => ['access content'],
       ],
-    ] + Webform::getDefaultAccessRules();
+    ] + $default_access_rules;
 
     Webform::load('contact')->setAccessRules($access_rules)->save();
 
diff --git a/web/modules/webform/tests/src/Functional/WebformBrowserTestBase.php b/web/modules/webform/tests/src/Functional/WebformBrowserTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..9651fbdd6b1ba9c36c4126307eee49daacf6e8d4
--- /dev/null
+++ b/web/modules/webform/tests/src/Functional/WebformBrowserTestBase.php
@@ -0,0 +1,475 @@
+<?php
+
+namespace Drupal\Tests\webform\Functional;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Config\FileStorage;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Serialization\Yaml;
+use Drupal\Core\Test\AssertMailTrait;
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\taxonomy\Entity\Term;
+use Drupal\taxonomy\Entity\Vocabulary;
+use Drupal\webform\WebformInterface;
+use Drupal\webform\Entity\Webform;
+
+/**
+ * Defines an abstract test base for webform tests.
+ */
+abstract class WebformBrowserTestBase extends BrowserTestBase {
+
+  use AssertMailTrait;
+  use WebformAssertLegacyTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = [];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->loadWebforms(static::$testWebforms);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function tearDown() {
+    $this->purgeSubmissions();
+    parent::tearDown();
+  }
+
+  /****************************************************************************/
+  // Block.
+  /****************************************************************************/
+
+  /**
+   * Place breadcrumb page, tasks, and actions.
+   */
+  protected function placeBlocks() {
+    $this->drupalPlaceBlock('system_breadcrumb_block');
+    $this->drupalPlaceBlock('page_title_block');
+    $this->drupalPlaceBlock('local_tasks_block');
+    $this->drupalPlaceBlock('local_actions_block');
+  }
+
+  /**
+   * Place webform test module blocks.
+   *
+   * @param string $module_name
+   *   Test module name.
+   */
+  protected function placeWebformBlocks($module_name) {
+    $config_directory = drupal_get_path('module', 'webform') . '/tests/modules/' . $module_name . '/config';
+    $config_files = file_scan_directory($config_directory, '/block\..*/');
+    foreach ($config_files as $config_file) {
+      $data = Yaml::decode(file_get_contents($config_file->uri));
+      $plugin_id = $data['plugin'];
+      $settings = $data['settings'];
+      unset($settings['id']);
+      $this->drupalPlaceBlock($plugin_id, $settings);
+    }
+  }
+
+  /****************************************************************************/
+  // Filter.
+  /****************************************************************************/
+
+  /**
+   * Basic HTML filter format.
+   *
+   * @var \Drupal\filter\FilterFormatInterface
+   */
+  protected $basicHtmlFilter;
+
+  /**
+   * Full HTML filter format.
+   *
+   * @var \Drupal\filter\FilterFormatInterface
+   */
+  protected $fullHtmlFilter;
+
+  /**
+   * Create basic HTML filter format.
+   */
+  protected function createFilters() {
+    $this->basicHtmlFilter = FilterFormat::create([
+      'format' => 'basic_html',
+      'name' => 'Basic HTML',
+      'filters' => [
+        'filter_html' => [
+          'status' => 1,
+          'settings' => [
+            'allowed_html' => '<p> <br> <strong> <a> <em>',
+          ],
+        ],
+      ],
+    ]);
+    $this->basicHtmlFilter->save();
+
+    $this->fullHtmlFilter = FilterFormat::create([
+      'format' => 'full_html',
+      'name' => 'Full HTML',
+    ]);
+    $this->fullHtmlFilter->save();
+  }
+
+  /****************************************************************************/
+  // Taxonomy.
+  /****************************************************************************/
+
+  /**
+   * Create the 'tags' taxonomy vocabulary.
+   */
+  protected function createTags() {
+    $vocabulary = Vocabulary::create([
+      'name' => 'Tags',
+      'vid' => 'tags',
+      'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+    ]);
+    $vocabulary->save();
+    for ($i = 1; $i <= 3; $i++) {
+      $parent_term = Term::create([
+        'name' => "Parent $i",
+        'vid' => 'tags',
+        'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+      ]);
+      $parent_term->save();
+      for ($x = 1; $x <= 3; $x++) {
+        $child_term = Term::create([
+          'name' => "Parent $i: Child $x",
+          'parent' => $parent_term->id(),
+          'vid' => 'tags',
+          'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+        ]);
+        $child_term->save();
+      }
+    }
+  }
+
+  /****************************************************************************/
+  // Webform.
+  /****************************************************************************/
+
+  /**
+   * Lazy load a test webforms.
+   *
+   * @param array $ids
+   *   Webform ids.
+   */
+  protected function loadWebforms(array $ids) {
+    foreach ($ids as $id) {
+      $this->loadWebform($id);
+    }
+    $this->pass(new FormattableMarkup('Loaded webforms: %webforms.', [
+      '%webforms' => implode(', ', $ids),
+    ]));
+  }
+
+  /**
+   * Lazy load a test webform.
+   *
+   * @param string $id
+   *   Webform id.
+   *
+   * @return \Drupal\webform\WebformInterface|null
+   *   A webform.
+   *
+   * @see \Drupal\views\Tests\ViewTestData::createTestViews
+   */
+  protected function loadWebform($id) {
+    $storage = \Drupal::entityTypeManager()->getStorage('webform');
+    if ($webform = $storage->load($id)) {
+      return $webform;
+    }
+    else {
+      $config_name = 'webform.webform.' . $id;
+      if (strpos($id, 'test_') === 0) {
+        $config_directory = drupal_get_path('module', 'webform') . '/tests/modules/webform_test/config/install';
+      }
+      elseif (strpos($id, 'example_') === 0) {
+        $config_directory = drupal_get_path('module', 'webform') . '/modules/webform_examples/config/install';
+      }
+      elseif (strpos($id, 'template_') === 0) {
+        $config_directory = drupal_get_path('module', 'webform') . '/modules/webform_templates/config/install';
+      }
+      else {
+        throw new \Exception("Webform $id not valid");
+      }
+
+      if (!file_exists("$config_directory/$config_name.yml")) {
+        throw new \Exception("Webform $id does not exist in $config_directory");
+      }
+
+      $file_storage = new FileStorage($config_directory);
+      $values = $file_storage->read($config_name);
+      $webform = $storage->create($values);
+      $webform->save();
+      return $webform;
+    }
+  }
+
+  /**
+   * Create a webform.
+   *
+   * @param array|null $values
+   *   (optional) Array of values.
+   * @param array|null $elements
+   *   (optional) Array of elements.
+   * @param array $settings
+   *   (optional) Webform settings.
+   *
+   * @return \Drupal\webform\WebformInterface
+   *   A webform.
+   */
+  protected function createWebform($values = [], array $elements = [], array $settings = []) {
+    // Create new webform.
+    $id = $this->randomMachineName(8);
+    $webform = Webform::create($values + [
+      'langcode' => 'en',
+      'status' => WebformInterface::STATUS_OPEN,
+      'id' => $id,
+      'title' => $id,
+      'elements' => Yaml::encode($elements),
+      'settings' => $settings + Webform::getDefaultSettings(),
+    ]);
+    $webform->save();
+    return $webform;
+  }
+
+  /****************************************************************************/
+  // Submission.
+  /****************************************************************************/
+
+  /**
+   * Post a new submission to a webform.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   * @param array $edit
+   *   Submission values.
+   * @param string $submit
+   *   Value of the submit button whose click is to be emulated.
+   * @param array $options
+   *   Options to be forwarded to the url generator.
+   *
+   * @return int
+   *   The created submission's sid.
+   */
+  protected function postSubmission(WebformInterface $webform, array $edit = [], $submit = NULL, array $options = []) {
+    $submit = $this->getWebformSubmitButtonLabel($webform, $submit);
+    $this->drupalPostForm('webform/' . $webform->id(), $edit, $submit, $options);
+    return $this->getLastSubmissionId($webform);
+  }
+
+  /**
+   * Post a new test submission to a webform.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   * @param array $edit
+   *   Submission values.
+   * @param string $submit
+   *   Value of the submit button whose click is to be emulated.
+   * @param array $options
+   *   Options to be forwarded to the url generator.
+   *
+   * @return int
+   *   The created test submission's sid.
+   */
+  protected function postSubmissionTest(WebformInterface $webform, array $edit = [], $submit = NULL, array $options = []) {
+    $submit = $this->getWebformSubmitButtonLabel($webform, $submit);
+    $this->drupalPostForm('webform/' . $webform->id() . '/test', $edit, $submit, $options);
+    return $this->getLastSubmissionId($webform);
+  }
+
+  /****************************************************************************/
+  // Submission.
+  /****************************************************************************/
+
+  /**
+   * Load the specified webform submission from the storage.
+   *
+   * @param int $sid
+   *   The submission identifier.
+   *
+   * @return \Drupal\webform\WebformSubmissionInterface
+   *   The loaded webform submission.
+   */
+  protected function loadSubmission($sid) {
+    /** @var \Drupal\webform\WebformSubmissionStorage $storage */
+    $storage = $this->container->get('entity_type.manager')->getStorage('webform_submission');
+    $storage->resetCache([$sid]);
+    return $storage->load($sid);
+  }
+
+  /**
+   * Purge all submission before the webform.module is uninstalled.
+   */
+  protected function purgeSubmissions() {
+    \Drupal::database()->query('DELETE FROM {webform_submission}');
+  }
+
+  /**
+   * Get a webform's submit button label.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   * @param string $submit
+   *   Value of the submit button whose click is to be emulated.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup|string
+   *   The webform's submit button label.
+   */
+  protected function getWebformSubmitButtonLabel(WebformInterface $webform, $submit = NULL) {
+    if ($submit) {
+      return $submit;
+    }
+
+    $actions_element = $webform->getElement('actions');
+    if ($actions_element && isset($actions_element['#submit__label'])) {
+      return $actions_element['#submit__label'];
+    }
+
+    return t('Submit');
+  }
+
+  /**
+   * Get the last submission id.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   *
+   * @return int|null
+   *   The last submission id. NULL if saving of results is disabled.
+   */
+  protected function getLastSubmissionId(WebformInterface $webform) {
+    if ($webform->getSetting('results_disabled')) {
+      return NULL;
+    }
+
+    // Get submission sid.
+    $url = UrlHelper::parse($this->getUrl());
+    if (isset($url['query']['sid'])) {
+      return $url['query']['sid'];
+    }
+    else {
+      $entity_ids = $this->container->get('entity_type.manager')->getStorage('webform_submission')->getQuery()
+        ->sort('sid', 'DESC')
+        ->condition('webform_id', $webform->id())
+        ->accessCheck(FALSE)
+        ->execute();
+      return reset($entity_ids);
+    }
+  }
+
+  /****************************************************************************/
+  // Export.
+  /****************************************************************************/
+
+  /**
+   * Request a webform results export CSV.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   * @param array $options
+   *   An associative array of export options.
+   */
+  protected function getExport(WebformInterface $webform, array $options = []) {
+    /** @var \Drupal\webform\WebformSubmissionExporterInterface $exporter */
+    $exporter = \Drupal::service('webform_submission.exporter');
+    $options += $exporter->getDefaultExportOptions();
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/download', ['query' => $options]);
+  }
+
+  /**
+   * Get webform export columns.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   *
+   * @return array
+   *   An array of exportable columns.
+   */
+  protected function getExportColumns(WebformInterface $webform) {
+    /** @var \Drupal\webform\WebformSubmissionStorageInterface $submission_storage */
+    $submission_storage = \Drupal::entityTypeManager()->getStorage('webform_submission');
+    $field_definitions = $submission_storage->getFieldDefinitions();
+    $field_definitions = $submission_storage->checkFieldDefinitionAccess($webform, $field_definitions);
+    $elements = $webform->getElementsInitializedAndFlattened();
+    $columns = array_merge(array_keys($field_definitions), array_keys($elements));
+    return array_combine($columns, $columns);
+  }
+
+  /****************************************************************************/
+  // Email.
+  /****************************************************************************/
+
+  /**
+   * Gets that last email sent during the currently running test case.
+   *
+   * @return array
+   *   An array containing the last email message captured during the
+   *   current test.
+   */
+  protected function getLastEmail() {
+    $sent_emails = $this->getMails();
+    $sent_email = end($sent_emails);
+    $this->debug($sent_email);
+    return $sent_email;
+  }
+
+  /****************************************************************************/
+  // Assert.
+  /****************************************************************************/
+
+  /**
+   * Passes if the CSS selector IS found on the loaded page, fail otherwise.
+   */
+  protected function assertCssSelect($selector, $message = '') {
+    $element = $this->cssSelect($selector);
+    if (!$message) {
+      $message = new FormattableMarkup('Found @selector', ['@selector' => $selector]);
+    }
+    $this->assertTrue(!empty($element), $message);
+  }
+
+  /**
+   * Passes if the CSS selector IS NOT found on the loaded page, fail otherwise.
+   */
+  protected function assertNoCssSelect($selector, $message = '') {
+    $element = $this->cssSelect($selector);
+    $this->assertTrue(empty($element), $message);
+  }
+
+  /****************************************************************************/
+  // Debug.
+  /****************************************************************************/
+
+  /**
+   * Logs verbose (debug) message in a text file.
+   *
+   * @param mixed $data
+   *   Data to be output.
+   */
+  protected function debug($data) {
+    $string = var_export($data, TRUE);
+    $string = preg_replace('/=>\s*array\s*\(/', '=> array(', $string);
+    $this->htmlOutput('<pre>' . htmlentities($string) . '</pre>');
+  }
+
+}
diff --git a/web/modules/webform/tests/src/Functional/WebformBrowserTestBaseTest.php b/web/modules/webform/tests/src/Functional/WebformBrowserTestBaseTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..d1cb8c4768fc0cbbd311bd1aad9d54d27dc9ee1e
--- /dev/null
+++ b/web/modules/webform/tests/src/Functional/WebformBrowserTestBaseTest.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Drupal\Tests\webform\Functional;
+
+use Drupal\webform\Entity\Webform;
+
+/**
+ * Test the webform test base class.
+ *
+ * @group webform_browser
+ */
+class WebformBrowserTestBaseTest extends WebformBrowserTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['webform', 'block', 'user'];
+
+  /**
+   * Webforms to load.
+   *
+   * @var array
+   */
+  protected static $testWebforms = ['test_ajax'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+  }
+
+  /**
+   * Test base  helper methods.
+   */
+  public function testWebformBase() {
+    // Check that test webform is installed.
+    $this->assertNotNull(Webform::load('test_ajax'));
+
+    // Check create webform.
+    $test_webform = $this->createWebform();
+    $this->assertNotNull($test_webform);
+
+    $webform = Webform::load('contact');
+
+    // Check post submission return NULL if post fails.
+    $sid = $this->postSubmission($webform);
+    $this->assertFalse($sid);
+
+    // Login root user.
+    $this->drupalLogin($this->rootUser);
+
+    // Check post test submission returns an sid.
+    $sid = $this->postSubmissionTest($webform);
+    $this->assertNotNull($sid);
+
+    // Check submission load not from cache.
+    $webform_submission = $this->loadSubmission($sid);
+    $this->assertNotNull($webform_submission);
+    $this->assertEquals($webform_submission->getWebform()->id(), 'contact');
+
+    // Check submission email.
+    $last_email = $this->getLastEmail();
+    $this->assertEquals($last_email['id'], 'webform_contact_email_notification');
+
+    // Check purge submission deletes the submission.
+    $this->purgeSubmissions();
+    $webform_submission = $this->loadSubmission($sid);
+    $this->assertNull($webform_submission);
+
+    // Check place blocks.
+    $this->placeBlocks();
+    $this->drupalGet('/webform/contact');
+    $this->assertRaw('block-system-breadcrumb-block');
+    $this->assertRaw('block-page-title-block');
+    $this->assertRaw('block-local-tasks-block');
+  }
+
+}
diff --git a/web/modules/webform/tests/src/Functional/WebformContributeFunctionalTest.php b/web/modules/webform/tests/src/Functional/WebformContributeFunctionalTest.php
index 8afe67e4f05d8653254d9211e3b7567f0da690a5..8d84130c8574d315518864aa17a30e39016eda32 100644
--- a/web/modules/webform/tests/src/Functional/WebformContributeFunctionalTest.php
+++ b/web/modules/webform/tests/src/Functional/WebformContributeFunctionalTest.php
@@ -52,7 +52,7 @@ public function testContribute() {
     $this->assertSession()->responseNotContains('<strong><a href="https://www.drupal.org/u/jrockowitz">Jacob Rockowitz</a></strong>');
 
     // Check that 'Contribute' local task is visible.
-    $this->drupalGet('/admin/structure/webform');
+    $this->drupalGet('/admin/structure/webform/help');
     $this->assertSession()->linkExists('Contribute');
 
     // Check that 'Contribute' route is accessible.
@@ -64,7 +64,7 @@ public function testContribute() {
     $this->drupalPostForm('/admin/structure/webform/config/advanced', $edit, t('Save'));
 
     // Check that 'Contribute' local task is hidden.
-    $this->drupalGet('/admin/structure/webform');
+    $this->drupalGet('/admin/structure/webform/help');
     $this->assertSession()->linkNotExists('Contribute');
 
     // Check that 'Contribute' route is removed.
diff --git a/web/modules/webform/tests/src/Functional/WebformListBuilderTest.php b/web/modules/webform/tests/src/Functional/WebformListBuilderTest.php
index 8e0d1bb92838892357ca0ed388c2513c3ec77994..2574bd310714baca051b7c2ed1e27dec1a8a4517 100644
--- a/web/modules/webform/tests/src/Functional/WebformListBuilderTest.php
+++ b/web/modules/webform/tests/src/Functional/WebformListBuilderTest.php
@@ -25,7 +25,7 @@ public function testWebformOverview() {
     $assert_session = $this->assertSession();
 
     // Test with a superuser.
-    $any_webform_user = $this->drupalCreateUser([
+    $any_webform_user = $this->createUser([
       'access webform overview',
       'create webform',
       'edit any webform',
@@ -35,18 +35,15 @@ public function testWebformOverview() {
     $list_path = '/admin/structure/webform';
     $this->drupalGet($list_path);
     $assert_session->linkExists('Test: Submissions');
-    $assert_session->linkExists('Submissions');
-    $assert_session->linkExists('Download');
-    $assert_session->linkExists('Clear');
+    $assert_session->linkExists('Results');
     $assert_session->linkExists('Build');
     $assert_session->linkExists('Settings');
     $assert_session->linkExists('View');
-    $assert_session->linkExists('Test');
     $assert_session->linkExists('Duplicate');
     $assert_session->linkExists('Delete');
 
     // Test with a user that only has submission access.
-    $any_webform_submission_user = $this->drupalCreateUser([
+    $any_webform_submission_user = $this->createUser([
       'access webform overview',
       'view any webform submission',
       'edit any webform submission',
@@ -57,15 +54,10 @@ public function testWebformOverview() {
     // Webform name should not be a link as the user doesn't have access to the
     // submission page.
     $assert_session->linkExists('Test: Submissions');
-    $assert_session->linkExists('Submissions');
-    $assert_session->linkExists('Download');
-    // TODO: Is this a bug that this doesn't pass? User has delete any
-    // submission permission.
-    // $assert_session->linkExists('Clear')
+    $assert_session->linkExists('Results');
     $assert_session->linkNotExists('Build');
     $assert_session->linkNotExists('Settings');
     $assert_session->linkExists('View');
-    $assert_session->linkExists('Test');
     $assert_session->linkNotExists('Duplicate');
     $assert_session->linkNotExists('Delete');
 
@@ -80,8 +72,8 @@ public function testWebformOverview() {
     $this->assertLinkNotInRow('Test: Submissions', 'View');
 
     // Test with role that is configured via webform access settings.
-    $rid = $this->createRole(['access webform overview']);
-    $special_access_user = $this->drupalCreateUser();
+    $rid = $this->drupalCreateRole(['access webform overview']);
+    $special_access_user = $this->createUser();
     $special_access_user->addRole($rid);
     $special_access_user->save();
     $access = $webform_config->get('access');
@@ -90,8 +82,7 @@ public function testWebformOverview() {
     $this->drupalLogin($special_access_user);
     $this->drupalGet($list_path);
     $assert_session->responseContains('Test: Submissions');
-    $assert_session->linkExists('Submissions');
-    $assert_session->linkExists('Download');
+    $assert_session->linkExists('Results');
   }
 
   /**
diff --git a/web/modules/webform/tests/src/Functional/WebformSubmissionViewsAccessTest.php b/web/modules/webform/tests/src/Functional/WebformSubmissionViewsAccessTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ff1bd8b7f9f7d79c3f0569cefa388b81efdd3ffd
--- /dev/null
+++ b/web/modules/webform/tests/src/Functional/WebformSubmissionViewsAccessTest.php
@@ -0,0 +1,199 @@
+<?php
+
+namespace Drupal\Tests\webform\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+use Drupal\webform\Entity\Webform;
+use Drupal\webform\Entity\WebformSubmission;
+use Drupal\webform\WebformInterface;
+
+/**
+ * Tests access rules in the context of webform submission views access.
+ *
+ * @group webform_browser
+ */
+class WebformSubmissionViewsAccessTest extends BrowserTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'system',
+    'user',
+    'views',
+    'webform',
+    'webform_test_views',
+  ];
+
+  /**
+   * Test webform submission entity access in a view query.
+   */
+  public function testEntityAccess() {
+    /** @var \Drupal\webform\WebformInterface $webform */
+    $webform = Webform::load('contact');
+
+    // Create any access user, own access user, and no (anonymous) access user.
+    $any_user = $this->drupalCreateUser(['access webform overview']);
+    $own_user = $this->drupalCreateUser(['access webform overview']);
+    $without_access_user = $this->drupalCreateUser(['access webform overview']);
+
+    // Grant any and own access to submissions.
+    $webform->setAccessRules([
+      'view_any' => ['users' => [$any_user->id()]],
+      'view_own' => ['users' => [$own_user->id()]],
+    ])->save();
+
+    // Create an array of the accounts.
+    $accounts = [
+      'any_user' => $any_user,
+      'own_user' => $own_user,
+      'without_access' => $without_access_user,
+    ];
+
+    // Create test submissions.
+    $this->createSubmissions($webform, $accounts);
+
+    // Check user submission access.
+    $this->checkUserSubmissionAccess($webform, $accounts);
+
+    // Clear webform access rules.
+    $webform->setAccessRules([])->save();
+
+    // Check user submission access cache is cleared.
+    $this->checkUserSubmissionAccess($webform, $accounts);
+  }
+
+  /**
+   * Tests webform submission views enforce access per user's permissions.
+   */
+  public function testPermissionAccess() {
+    /** @var \Drupal\webform\WebformInterface $webform */
+    $webform = Webform::load('contact');
+
+    // Create any access user, own access user, and no (anonymous) access user.
+    $own_webform_user = $this->drupalCreateUser([
+      'access webform overview',
+      'edit own webform',
+    ]);
+    $webform->setOwner($own_webform_user)->save();
+    $any_submission_user = $this->drupalCreateUser([
+      'access webform overview',
+      'view any webform submission',
+    ]);
+    $own_submission_user = $this->drupalCreateUser([
+      'access webform overview',
+      'view own webform submission',
+    ]);
+    $without_access_user = $this->drupalCreateUser([
+      'access webform overview',
+    ]);
+
+    // Create an array of the accounts.
+    /** @var \Drupal\user\Entity\User[] $accounts */
+    $accounts = [
+      'own_webform_user' => $own_webform_user,
+      'any_submission_user' => $any_submission_user,
+      'own_submission_user' => $own_submission_user,
+      'without_access' => $without_access_user,
+    ];
+
+    // Create test submissions.
+    $this->createSubmissions($webform, $accounts);
+
+    // Check user submission access.
+    $this->checkUserSubmissionAccess($webform, $accounts);
+
+    // Clear any and own permissions for all accounts.
+    foreach ($accounts as &$account) {
+      $roles = $account->getRoles(TRUE);
+      $rid = reset($roles);
+      user_role_revoke_permissions($rid, [
+        'view any webform submission',
+        'view own webform submission',
+        'edit own webform',
+      ]);
+    }
+
+    // Check user submission access cache is cleared.
+    $this->checkUserSubmissionAccess($webform, $accounts);
+  }
+
+  /**
+   * Create test a submission for each account.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   The webform.
+   * @param array $accounts
+   *   An associative array of test users.
+   */
+  protected function createSubmissions(WebformInterface $webform, array $accounts) {
+    /** @var \Drupal\webform\WebformSubmissionGenerateInterface $submission_generate */
+    $submission_generate = \Drupal::service('webform_submission.generate');
+
+    // Create a test submission for each user account.
+    foreach ($accounts as $account) {
+      WebformSubmission::create([
+        'webform_id' => $webform->id(),
+        'uid' => $account->id(),
+        'data' => $submission_generate->getData($webform),
+      ])->save();
+    }
+  }
+
+  /**
+   * Check user submission access.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   The webform.
+   * @param array $accounts
+   *   An associative array of test users.
+   *
+   * @see \Drupal\webform_access\Tests\WebformAccessSubmissionViewsTest::checkUserSubmissionAccess
+   */
+  protected function checkUserSubmissionAccess(WebformInterface $webform, array $accounts) {
+    /** @var \Drupal\webform\WebformSubmissionStorageInterface $webform_submission_storage */
+    $webform_submission_storage = \Drupal::entityTypeManager()
+      ->getStorage('webform_submission');
+
+    // Reset the static cache to make sure we are hitting actual fresh access
+    // results.
+    \Drupal::entityTypeManager()->getStorage('webform_submission')->resetCache();
+    \Drupal::entityTypeManager()->getAccessControlHandler('webform_submission')->resetCache();
+
+    foreach ($accounts as $account_type => $account) {
+      // Login the current user.
+      $this->drupalLogin($account);
+
+      // Get the webform_test_views_access view and the sid for each
+      // displayed record.  Submission access is controlled via the query.
+      // @see webform_query_webform_submission_access_alter()
+      $this->drupalGet('/admin/structure/webform/test/views_access');
+
+      $views_sids = [];
+      foreach ($this->getSession()->getPage()->findAll('css', '.view .view-content tbody .views-field-sid') as $node) {
+        $views_sids[] = $node->getText();
+      }
+      sort($views_sids);
+
+      $expected_sids = [];
+
+      // Load all webform submissions and check access using the access method.
+      // @see \Drupal\webform\WebformSubmissionAccessControlHandler::checkAccess
+      $webform_submissions = $webform_submission_storage->loadByEntities($webform);
+
+      foreach ($webform_submissions as $webform_submission) {
+        if ($webform_submission->access('view', $account)) {
+          $expected_sids[] = $webform_submission->id();
+        }
+      }
+
+      sort($expected_sids);
+
+      // Check that the views sids is equal to the expected sids.
+      $this->assertSame($expected_sids, $views_sids, "User '" . $account_type . "' access has correct access through view on webform submission entity type.");
+    }
+  }
+
+}
diff --git a/web/modules/webform/tests/src/FunctionalJavascript/WebformSubmissionToggleFlagsTest.php b/web/modules/webform/tests/src/FunctionalJavascript/WebformSubmissionToggleFlagsTest.php
index 9c69107519a6d2472970770a1a6832ca08621be3..3fa1338bbf10578db6c67d9cde593e0aeb6d5854 100644
--- a/web/modules/webform/tests/src/FunctionalJavascript/WebformSubmissionToggleFlagsTest.php
+++ b/web/modules/webform/tests/src/FunctionalJavascript/WebformSubmissionToggleFlagsTest.php
@@ -53,7 +53,7 @@ public function testHandlerJavascript() {
       'edit any webform submission',
       'delete any webform submission',
     ]));
-    $this->drupalGet('admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
+    $this->drupalGet('/admin/structure/webform/manage/' . $webform->id() . '/results/submissions');
     $assert->statusCodeEquals(200);
     $assert->elementExists('css', "#webform-submission-$sid-sticky")->click();
     $assert->assertWaitOnAjaxRequest();
diff --git a/web/modules/webform/tests/src/Kernel/Breadcrumb/WebformBreadcrumbBuilderTest.php b/web/modules/webform/tests/src/Kernel/Breadcrumb/WebformBreadcrumbBuilderTest.php
index fea93f8aa631f506408f9372633a4a45ad51d38f..ab485a881beaf4259ff082839ad4cf0783e12335 100644
--- a/web/modules/webform/tests/src/Kernel/Breadcrumb/WebformBreadcrumbBuilderTest.php
+++ b/web/modules/webform/tests/src/Kernel/Breadcrumb/WebformBreadcrumbBuilderTest.php
@@ -106,9 +106,9 @@ protected function setUp() {
     $this->setUpMockEntities();
 
     // Make some test doubles.
-    $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
-    $this->requestHandler = $this->getMock('Drupal\webform\WebformRequestInterface');
-    $this->translationManager = $this->getMock('Drupal\Core\StringTranslation\TranslationInterface');
+    $this->moduleHandler = $this->createMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $this->requestHandler = $this->createMock('Drupal\webform\WebformRequestInterface');
+    $this->translationManager = $this->createMock('Drupal\Core\StringTranslation\TranslationInterface');
 
     // Make an object to test.
     $this->breadcrumbBuilder = $this->getMockBuilder('Drupal\webform\Breadcrumb\WebformBreadcrumbBuilder')
@@ -197,13 +197,13 @@ public function providerTestApplies() {
       [FALSE, 'entity.webform'],
       [TRUE, 'entity.webform.handler.'],
       [TRUE, 'entity.webform_ui.element'],
-      [TRUE, 'webform.user.submissions'],
-      [TRUE, 'webform.user.submissions'],
+      [TRUE, 'entity.webform.user.submissions'],
       // Source entity.
       [TRUE, 'entity.{source_entity}.webform'],
       [TRUE, 'entity.{source_entity}.webform_submission'],
       [TRUE, 'entity.node.webform'],
       [TRUE, 'entity.node.webform_submission'],
+      [TRUE, 'entity.node.webform.user.submissions'],
       // Submissions.
       [FALSE, 'entity.webform.user.submission'],
       [TRUE, 'entity.webform.user.submission', [['webform_submission', $this->webformSubmissionAccess]]],
@@ -255,7 +255,7 @@ public function providerTestType() {
       // Handler.
       ['webform_handler', 'entity.webform.handler.'],
       // User submissions.
-      ['webform_user_submissions', 'webform.user.submissions'],
+      ['webform_user_submissions', 'entity.webform.user.submissions'],
       ['webform_source_entity', 'entity.{source_entity}.webform.user.submissions'],
       ['webform_source_entity', 'entity.node.webform.user.submissions'],
       // User submission.
@@ -492,7 +492,7 @@ protected function setSourceEntity(EntityInterface $entity) {
    *   A mocked route match.
    */
   protected function getMockRouteMatch($route_name = NULL, array $parameter_map = []) {
-    $route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
+    $route_match = $this->createMock('Drupal\Core\Routing\RouteMatchInterface');
     $route_match->expects($this->any())
       ->method('getRouteName')
       ->will($this->returnValue($route_name));
diff --git a/web/modules/webform/tests/src/Kernel/Entity/WebformEntityTest.php b/web/modules/webform/tests/src/Kernel/Entity/WebformEntityTest.php
index 28550932717fb5fa081af06824e6fbc50d974dee..74dd515760aac5b4d8a28b8990ca72754424ca55 100644
--- a/web/modules/webform/tests/src/Kernel/Entity/WebformEntityTest.php
+++ b/web/modules/webform/tests/src/Kernel/Entity/WebformEntityTest.php
@@ -2,9 +2,9 @@
 
 namespace Drupal\Tests\webform\Kernel\Entity;
 
-use Drupal\Core\Serialization\Yaml;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\webform\Entity\Webform;
+use Drupal\webform\Utility\WebformYaml;
 use Drupal\webform\WebformException;
 use Drupal\webform\WebformInterface;
 
@@ -193,7 +193,7 @@ public function testWebformMethods() {
     $webform->setElements($elements);
 
     // Check that elements are serialized to YAML.
-    $this->assertTrue($webform->getElementsRaw(), Yaml::encode($elements));
+    $this->assertTrue($webform->getElementsRaw(), WebformYaml::encode($elements));
 
     // Check elements decoded and flattened.
     $flattened_elements = [
@@ -212,7 +212,7 @@ public function testWebformMethods() {
     ];
     $this->assertEquals($webform->getElementsDecodedAndFlattened(), $flattened_elements);
 
-    // Check elements initialized  and flattened.
+    // Check elements initialized and flattened.
     $elements_initialized_and_flattened = [
       'root' => [
         '#type' => 'textfield',
@@ -353,7 +353,7 @@ public function testElementsCrud() {
       ],
     ];
     $webform->setElementProperties('root', $elements['root']);
-    $this->assertEquals($webform->getElementsRaw(), Yaml::encode($elements));
+    $this->assertEquals($webform->getElementsRaw(), WebformYaml::encode($elements));
 
     // Check add new container to root.
     $elements['root']['container'] = [
@@ -361,7 +361,7 @@ public function testElementsCrud() {
       '#title' => 'container',
     ];
     $webform->setElementProperties('container', $elements['root']['container'], 'root');
-    $this->assertEquals($webform->getElementsRaw(), Yaml::encode($elements));
+    $this->assertEquals($webform->getElementsRaw(), WebformYaml::encode($elements));
 
     // Check add new element to container.
     $elements['root']['container']['element'] = [
@@ -369,7 +369,7 @@ public function testElementsCrud() {
       '#title' => 'element',
     ];
     $webform->setElementProperties('element', $elements['root']['container']['element'], 'container');
-    $this->assertEquals($webform->getElementsRaw(), Yaml::encode($elements));
+    $this->assertEquals($webform->getElementsRaw(), WebformYaml::encode($elements));
 
     // Check delete container with al recursively delete all children.
     $elements = [
@@ -379,7 +379,7 @@ public function testElementsCrud() {
       ],
     ];
     $webform->deleteElement('container');
-    $this->assertEquals($webform->getElementsRaw(), Yaml::encode($elements));
+    $this->assertEquals($webform->getElementsRaw(), WebformYaml::encode($elements));
   }
 
 }
diff --git a/web/modules/webform/tests/src/Kernel/Utility/WebformDialogHelperTest.php b/web/modules/webform/tests/src/Kernel/Utility/WebformDialogHelperTest.php
index 27756c9c21e8c0e8ccf79d70eed03ae851052f7d..9a89615d59c6ff9deeb3a80cf0053f543f511bd6 100644
--- a/web/modules/webform/tests/src/Kernel/Utility/WebformDialogHelperTest.php
+++ b/web/modules/webform/tests/src/Kernel/Utility/WebformDialogHelperTest.php
@@ -34,14 +34,14 @@ public function testGetModalDialogAttributes() {
     $this->assertEquals(WebformDialogHelper::getModalDialogAttributes(), [
       'class' => ['webform-ajax-link'],
       'data-dialog-type' => 'modal',
-      'data-dialog-options' => '{"width":800,"dialogClass":"webform-modal"}',
+      'data-dialog-options' => '{"width":800,"dialogClass":"webform-ui-dialog"}',
     ]);
 
     // Check custom width and attributes.
     $this->assertEquals(WebformDialogHelper::getModalDialogAttributes(400, ['custom']), [
       'class' => ['custom', 'webform-ajax-link'],
       'data-dialog-type' => 'modal',
-      'data-dialog-options' => '{"width":400,"dialogClass":"webform-modal"}',
+      'data-dialog-options' => '{"width":400,"dialogClass":"webform-ui-dialog"}',
     ]);
 
     // Disable dialogs.
diff --git a/web/modules/webform/tests/src/Kernel/WebformEntityElementsValidationTest.php b/web/modules/webform/tests/src/Kernel/WebformEntityElementsValidationTest.php
index f8257a5b632a20a0247be5069a15d288f28b73d0..887a9f545190d24f91365b14ebea8dc6e3bcf0f5 100644
--- a/web/modules/webform/tests/src/Kernel/WebformEntityElementsValidationTest.php
+++ b/web/modules/webform/tests/src/Kernel/WebformEntityElementsValidationTest.php
@@ -64,6 +64,15 @@ public function testValidate() {
         ],
       ],
 
+      // Check names.
+      [
+        'getElementsRaw' => "Not Valid:
+  '#type': textfield",
+        'messages' => [
+          'The element key <em class="placeholder">Not Valid</em> on line 1 must contain only lowercase letters, numbers, and underscores.',
+        ],
+      ],
+
       // Check duplicate names.
       [
         'getElementsRaw' => "name:
@@ -88,6 +97,18 @@ public function testValidate() {
         ],
       ],
 
+      // Check reserved names.
+      [
+        'getElementsRaw' => "name:
+  '#type': textfield
+duplicate:
+  add:
+    '#type': textfield",
+        'messages' => [
+          'The element key <em class="placeholder">add</em> on line 4 is a reserved key.',
+        ],
+      ],
+
       // Check ignored properties.
       [
         'getElementsRaw' => "'tree':
@@ -189,7 +210,7 @@ public function testValidate() {
       ];
 
       /** @var \Drupal\webform\WebformInterface $webform */
-      $webform = $this->getMock('\Drupal\webform\WebformInterface');
+      $webform = $this->createMock('\Drupal\webform\WebformInterface');
       $methods = $test;
       unset($methods['messages']);
       foreach ($methods as $method => $returnValue) {
diff --git a/web/modules/webform/tests/src/Kernel/WebformSubmissionStorageTest.php b/web/modules/webform/tests/src/Kernel/WebformSubmissionStorageTest.php
index fdae99ff56a2751d2fec1a620709b489e183a3ec..d282318527eeb27abf00dba97c965eeb17ec959d 100644
--- a/web/modules/webform/tests/src/Kernel/WebformSubmissionStorageTest.php
+++ b/web/modules/webform/tests/src/Kernel/WebformSubmissionStorageTest.php
@@ -51,13 +51,13 @@ public function testStorage() {
     $this->assertEquals($webform_submission->id(), key($webform_submissions));
   }
 
-
   /**
    * Test purging of the webform submissions.
    *
    * @dataProvider providerPurge
    */
   public function testPurge($webform_purging, $webform_submissions_definition, $purged) {
+    $request_time = \Drupal::time()->getRequestTime();
     $days_to_seconds = 60 * 60 * 24;
     $purge_days = 10;
     $purge_amount = 2;
@@ -81,7 +81,7 @@ public function testPurge($webform_purging, $webform_submissions_definition, $pu
           'webform_id' => $v->id(),
         ]);
         $webform_submission->in_draft = $definition[0];
-        $webform_submission->setCreatedTime($definition[1] ? (REQUEST_TIME - ($purge_days + 1) * $days_to_seconds) : REQUEST_TIME);
+        $webform_submission->setCreatedTime($definition[1] ? ($request_time - ($purge_days + 1) * $days_to_seconds) : $request_time);
         $webform_submission->save();
       }
     }
@@ -91,11 +91,13 @@ public function testPurge($webform_purging, $webform_submissions_definition, $pu
     // Make sure nothing has been purged in the webform where purging is
     // disabled.
     $query = \Drupal::entityTypeManager()->getStorage('webform_submission')->getQuery();
+    $query->accessCheck(FALSE);
     $query->condition('webform_id', $webform_no_purging->id());
     $result = $query->execute();
     $this->assertEquals(count($webform_submissions_definition), count($result), 'No purging is executed when webform not not set up to purge.');
 
     $query = \Drupal::entityTypeManager()->getStorage('webform_submission')->getQuery();
+    $query->accessCheck(FALSE);
     $query->condition('webform_id', $webform->id());
     $result = [];
     foreach (\Drupal::entityTypeManager()->getStorage('webform_submission')->loadMultiple($query->execute()) as $submission) {
diff --git a/web/modules/webform/tests/src/Unit/Access/WebformAccessCheckTest.php b/web/modules/webform/tests/src/Unit/Access/WebformAccessCheckTest.php
deleted file mode 100644
index bf99006e0ffb4040a12f6c12e2fa30bf90700faf..0000000000000000000000000000000000000000
--- a/web/modules/webform/tests/src/Unit/Access/WebformAccessCheckTest.php
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-
-namespace Drupal\Tests\webform\Unit\Access;
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Tests\UnitTestCase;
-use Drupal\webform\Access\WebformAccountAccess;
-use Drupal\webform\Access\WebformSubmissionAccess;
-
-/**
- * @coversDefaultClass \Drupal\webform\Access\WebformAccountAccess
- *
- * @group webform
- */
-class WebformAccessCheckTest extends UnitTestCase {
-
-  /**
-   * The tested access checker.
-   *
-   * @var \Drupal\user\Access\PermissionAccessCheck
-   */
-  public $accessCheck;
-
-  /**
-   * The dependency injection container.
-   *
-   * @var \Symfony\Component\DependencyInjection\ContainerBuilder
-   */
-  protected $container;
-
-  /**
-   * Tests the check admin access.
-   *
-   * @covers ::checkAdminAccess
-   */
-  public function testCheckAdminAccess() {
-    $account = $this->getMock('Drupal\Core\Session\AccountInterface');
-
-    $admin_account = $this->getMock('Drupal\Core\Session\AccountInterface');
-    $admin_account->expects($this->any())
-      ->method('hasPermission')
-      ->will($this->returnValueMap([
-          ['administer webform', TRUE],
-          ['administer webform submission', TRUE],
-      ]
-      ));
-
-    $submission_manager_account = $this->getMock('Drupal\Core\Session\AccountInterface');
-    $submission_manager_account->expects($this->any())
-      ->method('hasPermission')
-      ->will($this->returnValueMap([
-          ['access webform overview', TRUE],
-          ['view any webform submission', TRUE],
-      ]
-      ));
-
-    $webform_node = $this->getMockBuilder('Drupal\node\NodeInterface')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $webform_node->expects($this->any())
-      ->method('access')
-      ->will($this->returnValue(TRUE));
-    $webform_node->expects($this->any())
-      ->method('hasField')
-      ->will($this->returnValue(TRUE));
-    $webform_node->webform = (object) ['entity' => TRUE];
-
-    $webform = $this->getMock('Drupal\webform\WebformInterface');
-
-    $email_webform = $this->getMock('Drupal\webform\WebformInterface');
-    $handler = $this->getMock('\Drupal\webform\Plugin\WebformHandlerMessageInterface');
-    $email_webform->expects($this->any())
-      ->method('getHandlers')
-      ->will($this->returnValue([$handler]));
-    $email_webform->expects($this->any())
-      ->method('access')
-      ->with('submission_update_any')
-      ->will($this->returnValue(TRUE));
-    $email_webform->expects($this->any())
-      ->method('hasMessageHandler')
-      ->will($this->returnValue(TRUE));
-
-    $webform_submission = $this->getMock('Drupal\webform\WebformSubmissionInterface');
-    $webform_submission->expects($this->any())
-      ->method('getWebform')
-      ->will($this->returnValue($webform));
-    $email_webform_submission = $this->getMock('Drupal\webform\WebformSubmissionInterface');
-    $email_webform_submission->expects($this->any())
-      ->method('getWebform')
-      ->will($this->returnValue($email_webform));
-
-    // Check submission access.
-    $this->assertEquals(AccessResult::neutral(), WebformAccountAccess::checkAdminAccess($account));
-    $this->assertEquals(AccessResult::allowed(), WebformAccountAccess::checkAdminAccess($admin_account));
-
-    // Check submission access.
-    $this->assertEquals(AccessResult::neutral(), WebformAccountAccess::checkSubmissionAccess($account));
-    $this->assertEquals(AccessResult::allowed(), WebformAccountAccess::checkSubmissionAccess($submission_manager_account));
-
-    // Check overview access.
-    $this->assertEquals(AccessResult::neutral(), WebformAccountAccess::checkOverviewAccess($account));
-    $this->assertEquals(AccessResult::allowed(), WebformAccountAccess::checkOverviewAccess($submission_manager_account));
-
-    // Check resend (email) message access.
-    $this->assertEquals(AccessResult::forbidden(), WebformSubmissionAccess::checkResendAccess($webform_submission, $account));
-    $this->assertEquals(AccessResult::allowed(), WebformSubmissionAccess::checkResendAccess($email_webform_submission, $submission_manager_account));
-
-    // @todo Fix below access check which is looping through the node's fields.
-    // Check entity results access.
-    // $this->assertEquals(AccessResult::neutral(), WebformSourceEntityAccess::checkEntityResultsAccess($node, $account));
-    // $this->assertEquals(AccessResult::allowed(), WebformSourceEntityAccess::checkEntityResultsAccess($webform_node, $submission_manager_account));
-  }
-
-}
diff --git a/web/modules/webform/tests/src/Unit/Access/WebformAccessTestBase.php b/web/modules/webform/tests/src/Unit/Access/WebformAccessTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..c02798a0489d06b8a89d0bcc85bbfd4f54e56f94
--- /dev/null
+++ b/web/modules/webform/tests/src/Unit/Access/WebformAccessTestBase.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\Tests\webform\Unit\Access;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Base class for test access checks.
+ */
+abstract class WebformAccessTestBase extends UnitTestCase {
+
+  /**
+   * The test container.
+   *
+   * @var \Symfony\Component\DependencyInjection\ContainerInterface
+   */
+  protected $container;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->container = new ContainerBuilder();
+    \Drupal::setContainer($this->container);
+
+    // Mock cache context manager and set container.
+    // @copied from \Drupal\Tests\Core\Access\AccessResultTest::setUp
+    $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE);
+    $this->container->set('cache_contexts_manager', $cache_contexts_manager);
+  }
+
+  /**
+   * Create a mock account with permissions.
+   *
+   * @param array $permissions
+   *   An associative array of permissions and results.
+   *
+   * @return \Drupal\Core\Session\AccountInterface
+   *   A mock account with ::hasPermission method.
+   */
+  protected function mockAccount(array $permissions = []) {
+    // Convert permission to value map.
+    $value_map = [];
+    foreach ($permissions as $permission => $result) {
+      $value_map[] = [$permission, $result];
+    }
+
+    $account = $this->createMock('Drupal\Core\Session\AccountInterface');
+
+    $account->expects($this->any())
+      ->method('hasPermission')
+      ->will($this->returnValueMap($value_map));
+
+    /** @var \Drupal\Core\Session\AccountInterface $account */
+    return $account;
+  }
+
+}
diff --git a/web/modules/webform/tests/src/Unit/Access/WebformAccountAccessTest.php b/web/modules/webform/tests/src/Unit/Access/WebformAccountAccessTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..99cf39c786c2ffa95b6b87cf66f432815c84e8eb
--- /dev/null
+++ b/web/modules/webform/tests/src/Unit/Access/WebformAccountAccessTest.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\Tests\webform\Unit\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\webform\Access\WebformAccountAccess;
+
+/**
+ * @coversDefaultClass \Drupal\webform\Access\WebformAccountAccess
+ *
+ * @group webform
+ */
+class WebformAccountAccessTest extends WebformAccessTestBase {
+
+  /**
+   * Tests the check webform account access.
+   *
+   * @covers ::checkAdminAccess
+   * @covers ::checkSubmissionAccess
+   * @covers ::checkOverviewAccess
+   */
+  public function testWebformAccountAccess() {
+    // Mock anonymous account.
+    $anonymous_account = $this->mockAccount();
+
+    // Mock admin account.
+    $admin_account = $this->mockAccount([
+      'administer webform' => TRUE,
+      'administer webform submission' => TRUE,
+    ]);
+
+    // Mock submission account.
+    $submission_account = $this->mockAccount([
+      'access webform overview' => TRUE,
+      'view any webform submission' => TRUE,
+    ]);
+
+    /**************************************************************************/
+
+    // Check admin access.
+    $this->assertEquals(AccessResult::neutral()->cachePerPermissions(), WebformAccountAccess::checkAdminAccess($anonymous_account)->setReason(''));
+    $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), WebformAccountAccess::checkAdminAccess($admin_account));
+
+    // Check submission access.
+    $this->assertEquals(AccessResult::neutral()->cachePerPermissions(), WebformAccountAccess::checkSubmissionAccess($anonymous_account)->setReason(''));
+    $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), WebformAccountAccess::checkSubmissionAccess($submission_account));
+
+    // Check overview access.
+    $this->assertEquals(AccessResult::neutral()->cachePerPermissions(), WebformAccountAccess::checkOverviewAccess($anonymous_account)->setReason(''));
+    $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), WebformAccountAccess::checkOverviewAccess($submission_account));
+  }
+
+}
diff --git a/web/modules/webform/tests/src/Unit/Access/WebformSourceEntityAccessTest.php b/web/modules/webform/tests/src/Unit/Access/WebformSourceEntityAccessTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..2f6ab1834bfebe335b6c437463980a7ff07b9b34
--- /dev/null
+++ b/web/modules/webform/tests/src/Unit/Access/WebformSourceEntityAccessTest.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\Tests\webform\Unit\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\webform\Access\WebformSourceEntityAccess;
+
+/**
+ * @coversDefaultClass \Drupal\webform\Access\WebformSourceEntityAccess
+ *
+ * @group webform
+ */
+class WebformSourceEntityAccessTest extends WebformAccessTestBase {
+
+  /**
+   * Tests the check webform source entity access.
+   *
+   * @covers ::checkEntityResultsAccess
+   */
+  public function testWebformSourceEntityAccess() {
+    // Mock anonymous account.
+    $anonymous_account = $this->mockAccount();
+
+    // Mock submission account.
+    $submission_account = $this->mockAccount([
+      'access webform overview' => TRUE,
+      'view any webform submission' => TRUE,
+    ]);
+
+    // Mock node.
+    $node = $this->getMockBuilder('Drupal\node\NodeInterface')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $node->expects($this->any())
+      ->method('access')
+      ->willReturn(AccessResult::neutral());
+
+    // Mock webform.
+    $webform = $this->createMock('Drupal\webform\WebformInterface');
+
+    // Mock webform node.
+    $webform_node = $this->getMockBuilder('Drupal\node\NodeInterface')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $webform_node->expects($this->any())
+      ->method('access')
+      ->willReturn(AccessResult::allowed());
+
+    // Mock entity reference manager.
+    $entity_reference_manager = $this->getMockBuilder('Drupal\webform\WebformEntityReferenceManagerInterface')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $entity_reference_manager->expects($this->any())
+      ->method('getWebform')
+      ->will($this->returnValueMap([
+        [$node, NULL],
+        [$webform_node, $webform],
+      ]));
+    $this->container->set('webform.entity_reference_manager', $entity_reference_manager);
+
+    /**************************************************************************/
+
+    // Check entity results access.
+    $this->assertEquals(AccessResult::neutral(), WebformSourceEntityAccess::checkEntityResultsAccess($node, $anonymous_account));
+    $this->assertEquals(AccessResult::allowed(), WebformSourceEntityAccess::checkEntityResultsAccess($webform_node, $submission_account));
+  }
+
+}
diff --git a/web/modules/webform/tests/src/Unit/Access/WebformSubmissionAccessTest.php b/web/modules/webform/tests/src/Unit/Access/WebformSubmissionAccessTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..459afc226461a314a8502d3e8a832671c6eff65f
--- /dev/null
+++ b/web/modules/webform/tests/src/Unit/Access/WebformSubmissionAccessTest.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Drupal\Tests\webform\Unit\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\webform\Access\WebformSubmissionAccess;
+
+/**
+ * @coversDefaultClass \Drupal\webform\Access\WebformSubmissionAccess
+ *
+ * @group webform
+ */
+class WebformSubmissionAccessTest extends WebformAccessTestBase {
+
+  /**
+   * Tests the check webform submission access.
+   *
+   * @covers ::checkResendAccess
+   * @covers ::checkWizardPagesAccess
+   */
+  public function testWebformSubmissionAccess() {
+    // Mock anonymous account.
+    $anonymous_account = $this->mockAccount();
+
+    // Mock submission account.
+    $submission_account = $this->mockAccount([
+      'access webform overview' => TRUE,
+      'view any webform submission' => TRUE,
+    ]);
+
+    // Mock webform.
+    $webform = $this->createMock('Drupal\webform\WebformInterface');
+
+    // Mock webform submission.
+    $webform_submission = $this->createMock('Drupal\webform\WebformSubmissionInterface');
+    $webform_submission->expects($this->any())
+      ->method('getWebform')
+      ->will($this->returnValue($webform));
+
+    // Mock message handler.
+    $message_handler = $this->createMock('\Drupal\webform\Plugin\WebformHandlerMessageInterface');
+
+    // Mock email webform.
+    $email_webform = $this->createMock('Drupal\webform\WebformInterface');
+    $email_webform->expects($this->any())
+      ->method('getHandlers')
+      ->will($this->returnValue([$message_handler]));
+    $email_webform->expects($this->any())
+      ->method('access')
+      ->with('submission_update_any')
+      ->will($this->returnValue(TRUE));
+    $email_webform->expects($this->any())
+      ->method('hasMessageHandler')
+      ->will($this->returnValue(TRUE));
+
+    // Mock email webform submission.
+    $email_webform_submission = $this->createMock('Drupal\webform\WebformSubmissionInterface');
+    $email_webform_submission->expects($this->any())
+      ->method('getWebform')
+      ->will($this->returnValue($email_webform));
+
+    // Mock webform wizard.
+    $webform_wizard = $this->createMock('Drupal\webform\WebformInterface');
+    $webform_wizard->expects($this->any())
+      ->method('hasWizardPages')
+      ->will($this->returnValue(TRUE));
+
+    // Mock webform wizard submission.
+    $webform_wizard_submission = $this->createMock('Drupal\webform\WebformSubmissionInterface');
+    $webform_wizard_submission->expects($this->any())
+      ->method('getWebform')
+      ->will($this->returnValue($webform_wizard));
+
+    /**************************************************************************/
+
+    // Check resend (email) message access.
+    $this->assertEquals(AccessResult::forbidden(), WebformSubmissionAccess::checkResendAccess($webform_submission, $anonymous_account));
+    $this->assertEquals(AccessResult::allowed(), WebformSubmissionAccess::checkResendAccess($email_webform_submission, $submission_account));
+
+    // Check wizard page access.
+    $this->assertEquals(AccessResult::neutral(), WebformSubmissionAccess::checkWizardPagesAccess($webform_submission));
+    $this->assertEquals(AccessResult::allowed(), WebformSubmissionAccess::checkWizardPagesAccess($webform_wizard_submission));
+
+  }
+
+}
diff --git a/web/modules/webform/tests/src/Unit/Plugin/Block/WebformBlockTest.php b/web/modules/webform/tests/src/Unit/Plugin/Block/WebformBlockTest.php
index 798578ab160a488afee31891bb150ca7ab230010..57786ec8b46246f690db3e8f18f170bad4f1f4c5 100644
--- a/web/modules/webform/tests/src/Unit/Plugin/Block/WebformBlockTest.php
+++ b/web/modules/webform/tests/src/Unit/Plugin/Block/WebformBlockTest.php
@@ -12,6 +12,7 @@
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformTokenManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Tests webform submission bulk form actions.
@@ -26,6 +27,7 @@ class WebformBlockTest extends UnitTestCase {
    * Tests the dependencies of a webform block.
    */
   public function testCalculateDependencies() {
+    // Create mock webform and webform block.
     $webform = $this->getMockBuilder(WebformInterface::class)
       ->disableOriginalConstructor()
       ->getMock();
@@ -35,35 +37,7 @@ public function testCalculateDependencies() {
       ->willReturn('config');
     $webform->method('getConfigDependencyName')
       ->willReturn('config.webform.' . $webform->id());
-
-    $entity_type_manager = $this->getMockBuilder(EntityTypeManagerInterface::class)
-      ->disableOriginalConstructor()
-      ->getMock();
-
-    $storage = $this->getMockBuilder(EntityStorageInterface::class)
-      ->disableOriginalConstructor()
-      ->getMock();
-
-    $entity_type_manager->method('getStorage')
-      ->willReturnMap([
-        ['webform', $storage],
-      ]);
-
-    $storage->method('load')
-      ->willReturnMap([
-        [$webform->id(), $webform],
-      ]);
-
-    $token_manager = $this->getMockBuilder(WebformTokenManagerInterface::class)
-      ->disableOriginalConstructor()
-      ->getMock();
-
-    $block = new WebformBlock([
-      'webform_id' => $webform->id(),
-      'default_data' => [],
-    ], 'webform_block', [
-      'provider' => 'unit_test',
-    ], $entity_type_manager, $token_manager);
+    $block = $this->mockWebformBlock($webform);
 
     $dependencies = $block->calculateDependencies();
     $expected = [
@@ -105,6 +79,7 @@ public function testBlockAccess() {
     $access_result->addCacheTags(['dummy_cache_tag']);
     $access_result->addCacheContexts($cache_contexts);
 
+    // Create mock webform and webform block.
     $webform = $this->getMockBuilder(WebformInterface::class)
       ->disableOriginalConstructor()
       ->getMock();
@@ -114,6 +89,30 @@ public function testBlockAccess() {
       ->willReturnMap([
         ['submission_create', $account, TRUE, $access_result],
       ]);
+    $block = $this->mockWebformBlock($webform);
+
+    $result = $block->access($account, TRUE);
+
+    // Make sure the block transparently follows the webform access logic.
+    $this->assertSame($access_result->isAllowed(), $result->isAllowed(), 'Block access yields the same result as the access of the webform.');
+    $this->assertEquals($access_result->getCacheContexts(), $result->getCacheContexts(), 'Block access has the same cache contexts as the access of the webform.');
+    $this->assertEquals($access_result->getCacheTags(), $result->getCacheTags(), 'Block access has the same cache tags as the access of the webform.');
+    $this->assertEquals($access_result->getCacheMaxAge(), $result->getCacheMaxAge(), 'Block access has the same cache max age as the access of the webform.');
+  }
+
+  /**
+   * Create a mock webform block.
+   *
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A webform.
+   *
+   * @return \Drupal\webform\Plugin\Block\WebformBlock
+   *   A mock webform block.
+   */
+  protected function mockWebformBlock(WebformInterface $webform) {
+    $request_stack = $this->getMockBuilder(RequestStack::class)
+      ->disableOriginalConstructor()
+      ->getMock();
 
     $entity_type_manager = $this->getMockBuilder(EntityTypeManagerInterface::class)
       ->disableOriginalConstructor()
@@ -122,35 +121,27 @@ public function testBlockAccess() {
     $storage = $this->getMockBuilder(EntityStorageInterface::class)
       ->disableOriginalConstructor()
       ->getMock();
-
-    $entity_type_manager->method('getStorage')
+    $storage->method('load')
       ->willReturnMap([
-        ['webform', $storage],
+        [$webform->id(), $webform],
       ]);
 
-    $storage->method('load')
+    $entity_type_manager->method('getStorage')
       ->willReturnMap([
-        [$webform->id(), $webform],
+        ['webform', $storage],
       ]);
 
     $token_manager = $this->getMockBuilder(WebformTokenManagerInterface::class)
       ->disableOriginalConstructor()
       ->getMock();
 
-    $block = new WebformBlock([
-      'webform_id' => $webform->id(),
-      'default_data' => [],
-    ], 'webform_block', [
-      'provider' => 'unit_test',
-    ], $entity_type_manager, $token_manager);
+    $configuration = ['webform_id' => $webform->id()];
 
-    $result = $block->access($account, TRUE);
+    $plugin_id = 'webform_block';
 
-    // Make sure the block transparently follows the webform access logic.
-    $this->assertSame($access_result->isAllowed(), $result->isAllowed(), 'Block access yields the same result as the access of the webform.');
-    $this->assertEquals($access_result->getCacheContexts(), $result->getCacheContexts(), 'Block access has the same cache contexts as the access of the webform.');
-    $this->assertEquals($access_result->getCacheTags(), $result->getCacheTags(), 'Block access has the same cache tags as the access of the webform.');
-    $this->assertEquals($access_result->getCacheMaxAge(), $result->getCacheMaxAge(), 'Block access has the same cache max age as the access of the webform.');
+    $plugin_definition = ['provider' => 'unit_test'];
+
+    return new WebformBlock($configuration, $plugin_id, $plugin_definition, $request_stack, $entity_type_manager, $token_manager);
   }
 
 }
diff --git a/web/modules/webform/tests/src/Unit/Plugin/WebformSourceEntity/QueryStringWebformSourceEntityTest.php b/web/modules/webform/tests/src/Unit/Plugin/WebformSourceEntity/QueryStringWebformSourceEntityTest.php
index e3528003c512348d47db2c5997949d660967cb87..fceba7fd9033f00170de90bd2b4b7ee10e22c459 100644
--- a/web/modules/webform/tests/src/Unit/Plugin/WebformSourceEntity/QueryStringWebformSourceEntityTest.php
+++ b/web/modules/webform/tests/src/Unit/Plugin/WebformSourceEntity/QueryStringWebformSourceEntityTest.php
@@ -6,7 +6,6 @@
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Language\Language;
-use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\webform\Plugin\WebformSourceEntity\QueryStringWebformSourceEntity;
@@ -28,22 +27,8 @@ class QueryStringWebformSourceEntityTest extends UnitTestCase {
   /**
    * Tests detection of source entity via query string.
    *
-   * @param bool $webform_in_route
-   *   Whether webform should be included in route object.
-   * @param string $source_entity_type_in_query
-   *   Source entity type to include into query string.
-   * @param bool $source_entity_view_access
-   *   Whether 'view' access should be allowed in the source entity.
-   * @param bool $webform_prepopulate_source_entity
-   *   Value for the setting 'form_prepopulate_source_entity' of the webform.
-   * @param bool $source_entity_references_webform
-   *   Whether the source entity should reference webform.
-   * @param bool $source_entity_has_translation
-   *   Whether the source entity should have a translation and (whenever
-   *   $source_entity_references_webform is TRUE) refer the webform from that
-   *   translation.
-   * @param string[] $ignored_types
-   *   Array of entity types that may not be source.
+   * @param array $options
+   *   see ::providerGetCurrentSourceEntity.
    * @param bool $expect_source_entity
    *   Whether we expect the tested method to return the source entity.
    * @param string $assert_message
@@ -53,136 +38,178 @@ class QueryStringWebformSourceEntityTest extends UnitTestCase {
    *
    * @dataProvider providerGetCurrentSourceEntity
    */
-  public function testGetCurrentSourceEntity($webform_in_route, $source_entity_type_in_query, $source_entity_view_access, $webform_prepopulate_source_entity, $source_entity_references_webform, $source_entity_has_translation, array $ignored_types, $expect_source_entity, $assert_message = '') {
-    $source_entity_type = 'node';
-    $source_entity_id = 1;
+  public function testGetCurrentSourceEntity(array $options, $expect_source_entity, $assert_message = '') {
+    $options += [
+      // Value for the setting 'form_prepopulate_source_entity' of the webform.
+      'webform_settings_prepopulate_source_entity' => TRUE,
+
+      // Source entity type.
+      'source_entity_type' => 'node',
+      // Source entity id.
+      'source_entity_id' => 1,
+       // Access result return by source entity 'view' operation.
+      'source_entity_view_access_result' => TRUE,
+      // Whether source entity has a populate webform field.
+      'source_entity_has_webform_field' => TRUE,
+       // Whether the source entity has translation.
+      'source_entity_has_translation' => TRUE,
+
+       // Source entity type return by request query string.
+      'request_query_source_entity_type' => 'node',
+
+       // Whether webform should be included in route object.
+      'route_match_get_parameter_webform' => TRUE,
+
+      // Array of entity types that may not be source.
+      'ignored_types' => [],
+    ];
+
+    /**************************************************************************/
+
+    $webform = $this->getMockWebform($options);
+    list($source_entity, $source_entity_translation) = $this->getMockSourceEntity($options, $webform);
+
+    // Mock source entity storage.
+    $source_entity_storage = $this->getMockBuilder(EntityStorageInterface::class)
+      ->getMock();
+    $source_entity_storage->method('load')
+      ->willReturnMap([
+        [$options['source_entity_id'], $source_entity],
+      ]);
 
+    // Move entity type manager which returns the mock source entity storage.
     $entity_type_manager = $this->getMockBuilder(EntityTypeManagerInterface::class)
       ->disableOriginalConstructor()
       ->getMock();
+    $entity_type_manager->method('hasDefinition')
+      ->willReturnMap([
+        [$options['source_entity_type'], TRUE],
+      ]);
+    $entity_type_manager->method('getStorage')
+      ->willReturnMap([
+        [$options['source_entity_type'], $source_entity_storage],
+      ]);
 
+    // Mock route match.
     $route_match = $this->getMockBuilder(RouteMatchInterface::class)
       ->disableOriginalConstructor()
       ->getMock();
+    $route_match->method('getParameter')
+      ->willReturnMap([
+        ['webform', $options['route_match_get_parameter_webform'] ? $webform : NULL],
+      ]);
 
+    // Mock request stack.
     $request_stack = $this->getMockBuilder(RequestStack::class)
       ->disableOriginalConstructor()
       ->getMock();
-
+    $request_stack->method('getCurrentRequest')
+      ->will($this->returnValue(
+        new Request([
+          'source_entity_type' => $options['request_query_source_entity_type'],
+          'source_entity_id' => $options['source_entity_id'],
+        ])
+      ));
+
+    // Move entity reference manager.
     $webform_entity_reference_manager = $this->getMockBuilder(WebformEntityReferenceManagerInterface::class)
       ->disableOriginalConstructor()
       ->getMock();
+    $webform_entity_reference_manager->method('getFieldNames')
+      ->willReturnMap([
+        [$source_entity, ['webform_field_name']],
+        [$source_entity_translation, ['webform_field_name']],
+      ]);
 
+    // Mock language manager.
     $language_manager = $this->getMockBuilder(LanguageManagerInterface::class)
       ->disableOriginalConstructor()
       ->getMock();
+    $language_manager->method('getCurrentLanguage')
+      ->willReturn(new Language(['id' => 'es']));
 
-    $webform = $this->getMockBuilder(WebformInterface::class)
-      ->disableOriginalConstructor()
-      ->getMock();
-
-    $request = new Request([
-      'source_entity_type' => $source_entity_type_in_query,
-      'source_entity_id' => $source_entity_id,
-    ]);
+    /**************************************************************************/
 
-    $source_entity_storage = $this->getMockBuilder(EntityStorageInterface::class)
-      ->getMock();
+    // Create QueryStringWebformSourceEntity plugin instance.
+    $plugin = new QueryStringWebformSourceEntity([], 'query_string', [], $entity_type_manager, $route_match, $request_stack, $language_manager, $webform_entity_reference_manager);
 
-    $source_entity = $this->getMockBuilder(ContentEntityInterface::class)
-      ->getMock();
+    $output = $plugin->getSourceEntity($options['ignored_types']);
+    if ($expect_source_entity) {
+      $this->assertSame($options['source_entity_has_translation'] ? $source_entity_translation : $source_entity, $output, $assert_message);
+    }
+    else {
+      $this->assertNull($output, $assert_message);
+    }
+  }
 
-    $source_entity_translation = $this->getMockBuilder(ContentEntityInterface::class)
+  /**
+   * Get mock webform entity.
+   *
+   * @param array $options
+   *   Mock webform options.
+   *
+   * @return \PHPUnit\Framework\MockObject\MockObject
+   *   A mocked webform entity.
+   */
+  protected function getMockWebform(array $options) {
+    $webform = $this->getMockBuilder(WebformInterface::class)
+      ->disableOriginalConstructor()
       ->getMock();
-
-    $route_match->method('getParameter')
-      ->will($this->returnValueMap([
-        ['webform', $webform_in_route ? $webform : NULL],
-      ]));
-
-    $request_stack->method('getCurrentRequest')
-      ->will($this->returnValue($request));
-
-    $entity_type_manager->method('hasDefinition')
-      ->willReturnMap([
-        [$source_entity_type, TRUE],
-      ]);
-
-    $entity_type_manager->method('getStorage')
-      ->willReturnMap([
-        [$source_entity_type, $source_entity_storage],
-      ]);
-
-    $source_entity_storage->method('load')
-      ->willReturnMap([
-        [$source_entity_id, $source_entity],
-      ]);
-
-    $source_entity_storage->method('load')
-      ->willReturnMap([
-        [$source_entity_id, $source_entity],
-      ]);
-
-    $source_entity->method('access')
-      ->willReturnMap([
-        ['view', NULL, FALSE, $source_entity_view_access],
-      ]);
-
-    $source_entity_translation->method('access')
-      ->willReturnMap([
-        ['view', NULL, FALSE, $source_entity_view_access],
-      ]);
-
     $webform->method('getSetting')
       ->willReturnMap([
-        ['form_prepopulate_source_entity', FALSE, $webform_prepopulate_source_entity],
+        ['form_prepopulate_source_entity', FALSE, $options['webform_settings_prepopulate_source_entity']],
       ]);
-
     $webform->method('id')
       ->willReturn('webform_id');
+    return $webform;
+  }
 
-    $webform_entity_reference_manager->method('getFieldName')
+  /**
+   * Get mocked source entity.
+   *
+   * @param array $options
+   *   Mock source entity options.
+   * @param \Drupal\webform\WebformInterface $webform
+   *   A mocked webform.
+   *
+   * @return array
+   *   An array containing a mocked source entity and its
+   *   translated source entity.
+   */
+  protected function getMockSourceEntity(array $options, WebformInterface $webform) {
+    // Mock source entity.
+    $source_entity = $this->getMockBuilder(ContentEntityInterface::class)
+      ->getMock();
+    $source_entity->method('access')
       ->willReturnMap([
-        [$source_entity, 'webform_field_name'],
-        [$source_entity_translation, 'webform_field_name'],
+        ['view', NULL, FALSE, $options['source_entity_view_access_result']],
       ]);
-
-    $language_manager->method('getCurrentLanguage')
-      ->willReturn(new Language([
-        'id' => 'ua',
-        'name' => 'Ukrainian',
-        'direction' => LanguageInterface::DIRECTION_LTR,
-        'weight' => 0,
-        'locked' => FALSE,
-      ]));
-
-    $source_entity->webform_field_name = (object) [
-      'target_id' => $source_entity_references_webform && !$source_entity_has_translation ? $webform->id() : 'other_webform',
+    $source_entity->webform_field_name = [
+      (object) ['target_id' => $options['source_entity_has_webform_field'] && !$options['source_entity_has_translation'] ? $webform->id() : 'other_webform'],
     ];
 
-    $source_entity_translation->webform_field_name = (object) [
-      'target_id' => $source_entity_references_webform && $source_entity_has_translation ? $webform->id() : 'other_webform',
+    // Mock source entity translation.
+    $source_entity_translation = $this->getMockBuilder(ContentEntityInterface::class)
+      ->getMock();
+    $source_entity_translation->method('access')
+      ->willReturnMap([
+        ['view', NULL, FALSE, $options['source_entity_view_access_result']],
+      ]);
+    $source_entity_translation->webform_field_name = [
+      (object) ['target_id' => $options['source_entity_has_webform_field'] && $options['source_entity_has_translation'] ? $webform->id() : 'other_webform'],
     ];
 
+    // Add translation to source entity.
     $source_entity->method('hasTranslation')
       ->willReturnMap([
-        [$language_manager->getCurrentLanguage()->getId(), $source_entity_has_translation],
+        ['es', $options['source_entity_has_translation']],
       ]);
-
     $source_entity->method('getTranslation')
       ->willReturnMap([
-        [$language_manager->getCurrentLanguage()->getId(), $source_entity_translation],
+        ['es', $source_entity_translation],
       ]);
 
-    $plugin = new QueryStringWebformSourceEntity([], 'query_string', [], $entity_type_manager, $route_match, $request_stack, $language_manager, $webform_entity_reference_manager);
-    $output = $plugin->getSourceEntity($ignored_types);
-
-    if ($expect_source_entity) {
-      $this->assertSame($source_entity_has_translation ? $source_entity_translation : $source_entity, $output, $assert_message);
-    }
-    else {
-      $this->assertNull($output, $assert_message);
-    }
+    return [$source_entity, $source_entity_translation];
   }
 
   /**
@@ -191,16 +218,85 @@ public function testGetCurrentSourceEntity($webform_in_route, $source_entity_typ
    * @see testGetCurrentSourceEntity()
    */
   public function providerGetCurrentSourceEntity() {
-    $tests[] = [FALSE, 'node', TRUE, TRUE, TRUE, FALSE, [], FALSE, 'No webform in route'];
-    $tests[] = [TRUE, 'user', TRUE, TRUE, TRUE, FALSE, [], FALSE, 'Inexisting entity type in query string'];
-    $tests[] = [TRUE, 'node', FALSE, TRUE, TRUE, FALSE, [], FALSE, 'Source entity without "view" access'];
-    $tests[] = [TRUE, 'node', FALSE, TRUE, TRUE, TRUE, [], FALSE, 'Source entity translated without "view" access'];
-    $tests[] = [TRUE, 'node', TRUE, TRUE, TRUE, FALSE, [], TRUE, 'Prepopulating of webform source entity is allowed'];
-    $tests[] = [TRUE, 'node', TRUE, TRUE, TRUE, FALSE, ['node'], TRUE, '$ignored_types is not considered'];
-    $tests[] = [TRUE, 'node', TRUE, FALSE, TRUE, FALSE, [], TRUE, 'Source entity references webform'];
-    $tests[] = [TRUE, 'node', TRUE, FALSE, TRUE, TRUE, [], TRUE, 'Translation of source entity references webform'];
-    $tests[] = [TRUE, 'node', TRUE, FALSE, FALSE, FALSE, [], FALSE, 'Source entity does not reference webform'];
-    $tests[] = [TRUE, 'node', TRUE, FALSE, FALSE, TRUE, [], FALSE, 'Translation of source entity does not reference webform'];
+    $tests[] = [
+      [
+        'source_entity_has_translation' => FALSE,
+        'route_match_get_parameter_webform' => FALSE,
+      ],
+      FALSE,
+      'No webform in route',
+    ];
+    $tests[] = [
+      [
+        'source_entity_has_translation' => FALSE,
+        'request_query_source_entity_type' => 'user',
+      ],
+      FALSE,
+      'Inexisting entity type in query string',
+    ];
+    $tests[] = [
+      [
+        'source_entity_view_access_result' => FALSE,
+        'source_entity_has_translation' => FALSE,
+      ],
+      FALSE,
+      'Source entity without "view" access',
+    ];
+    $tests[] = [
+      [
+        'source_entity_view_access_result' => FALSE,
+      ],
+      FALSE,
+      'Source entity translated without "view" access',
+    ];
+    $tests[] = [
+      [
+        'source_entity_has_translation' => FALSE,
+      ],
+      TRUE,
+      'Prepopulating of webform source entity is allowed',
+    ];
+    $tests[] = [
+      [
+        'source_entity_has_translation' => FALSE,
+        'ignored_types' => ['node'],
+      ],
+      TRUE,
+      'Ignored_types is not considered by query string plugin.',
+    ];
+    $tests[] = [
+      [
+        'webform_settings_prepopulate_source_entity' => FALSE,
+        'source_entity_has_translation' => FALSE,
+      ],
+      TRUE,
+      'Source entity references webform',
+    ];
+    $tests[] = [
+      [
+        'webform_settings_prepopulate_source_entity' => FALSE,
+      ],
+      TRUE,
+      'Translation of source entity references webform',
+    ];
+    $tests[] = [
+      [
+        'webform_settings_prepopulate_source_entity' => FALSE,
+        'source_entity_has_webform_field' => FALSE,
+        'source_entity_has_translation' => FALSE,
+      ],
+      FALSE,
+      'Source entity does not reference webform',
+    ];
+    $tests[] = [
+      [
+        'webform_settings_prepopulate_source_entity' => FALSE,
+        'source_entity_has_webform_field' => FALSE,
+      ],
+      FALSE,
+      'Translation of source entity does not reference webform',
+    ];
+
     return $tests;
   }
 
diff --git a/web/modules/webform/tests/src/Unit/Plugin/views/field/WebformSubmissionBulkFormTest.php b/web/modules/webform/tests/src/Unit/Plugin/views/field/WebformSubmissionBulkFormTest.php
index 391da65d108391f4a9a82206bf55663cea25bd24..0b62cfc14740ae0fa267804cb5bacff29295a186 100644
--- a/web/modules/webform/tests/src/Unit/Plugin/views/field/WebformSubmissionBulkFormTest.php
+++ b/web/modules/webform/tests/src/Unit/Plugin/views/field/WebformSubmissionBulkFormTest.php
@@ -32,31 +32,33 @@ public function testConstructor() {
     $actions = [];
 
     for ($i = 1; $i <= 2; $i++) {
-      $action = $this->getMock('\Drupal\system\ActionConfigEntityInterface');
+      $action = $this->createMock('\Drupal\system\ActionConfigEntityInterface');
       $action->expects($this->any())
         ->method('getType')
         ->will($this->returnValue('webform_submission'));
       $actions[$i] = $action;
     }
 
-    $action = $this->getMock('\Drupal\system\ActionConfigEntityInterface');
+    $action = $this->createMock('\Drupal\system\ActionConfigEntityInterface');
     $action->expects($this->any())
       ->method('getType')
       ->will($this->returnValue('user'));
     $actions[] = $action;
 
-    $entity_storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
+    $entity_storage = $this->createMock('Drupal\Core\Entity\EntityStorageInterface');
     $entity_storage->expects($this->any())
       ->method('loadMultiple')
       ->will($this->returnValue($actions));
 
-    $entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
+    $entity_manager = $this->createMock('Drupal\Core\Entity\EntityManagerInterface');
     $entity_manager->expects($this->once())
       ->method('getStorage')
       ->with('action')
       ->will($this->returnValue($entity_storage));
 
-    $language_manager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
+    $language_manager = $this->createMock('Drupal\Core\Language\LanguageManagerInterface');
+
+    $messenger = $this->createMock('\Drupal\Core\Messenger\MessengerInterface');
 
     $views_data = $this->getMockBuilder('Drupal\views\ViewsData')
       ->disableOriginalConstructor()
@@ -70,7 +72,7 @@ public function testConstructor() {
     $container->set('string_translation', $this->getStringTranslationStub());
     \Drupal::setContainer($container);
 
-    $storage = $this->getMock('Drupal\views\ViewEntityInterface');
+    $storage = $this->createMock('Drupal\views\ViewEntityInterface');
     $storage->expects($this->any())
       ->method('get')
       ->with('base_table')
@@ -88,7 +90,7 @@ public function testConstructor() {
     $definition['title'] = '';
     $options = [];
 
-    $webform_submission_bulk_form = new WebformSubmissionBulkForm([], 'webform_submission_bulk_form', $definition, $entity_manager, $language_manager);
+    $webform_submission_bulk_form = new WebformSubmissionBulkForm([], 'webform_submission_bulk_form', $definition, $entity_manager, $language_manager, $messenger);
     $webform_submission_bulk_form->init($executable, $display, $options);
 
     $this->assertAttributeEquals(array_slice($actions, 0, -1, TRUE), 'actions', $webform_submission_bulk_form);
diff --git a/web/modules/webform/tests/src/Unit/Utility/WebformDateHelperTest.php b/web/modules/webform/tests/src/Unit/Utility/WebformDateHelperTest.php
deleted file mode 100644
index de1d1b005216c091161f6cf65eed15bec17cc82c..0000000000000000000000000000000000000000
--- a/web/modules/webform/tests/src/Unit/Utility/WebformDateHelperTest.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-namespace Drupal\Tests\webform\Unit\Utility;
-
-use Drupal\webform\Utility\WebformDateHelper;
-use Drupal\Tests\UnitTestCase;
-
-/**
- * Tests webform date helper utility.
- *
- * @group webform
- *
- * @coversDefaultClass \Drupal\webform\Utility\WebformDateHelper
- */
-class WebformDateHelperTest extends UnitTestCase {
-
-  /**
-   * Tests WebformDateHelper::isValidDateFormat().
-   *
-   * @param string $time
-   *   The date/time string to run through WebformDateHelper::isValidDateFormat().
-   * @param string $format
-   *   Format accepted by date().
-   * @param string $expected
-   *   The expected result from calling the function.
-   *
-   * @see WebformDateHelper::isValidDateFormat()
-   *
-   * @dataProvider providerisValidDateFormat
-   */
-  public function testisValidDateFormat($time, $format, $expected) {
-    $result = WebformDateHelper::isValidDateFormat($time, $format);
-    $this->assertEquals($expected, $result);
-  }
-
-  /**
-   * Data provider for testisValidDateFormat().
-   *
-   * @see testisValidDateFormat()
-   */
-  public function providerisValidDateFormat() {
-    $tests[] = ['2013-13-01', 'Y-m-d', FALSE];
-    $tests[] = ['2013-13-01', 'Y-m-d', FALSE];
-    $tests[] = ['20132-13-01', 'Y-m-d', FALSE];
-    $tests[] = ['2013-11-32', 'Y-m-d', FALSE];
-    $tests[] = ['2012-2-25', 'Y-m-d', FALSE];
-    $tests[] = ['2013-12-01', 'Y-m-d', TRUE];
-    $tests[] = ['1970-12-01', 'Y-m-d', TRUE];
-    $tests[] = ['2012-02-29', 'Y-m-d', TRUE];
-    $tests[] = ['2013-13-01', 'Y-m-d', FALSE];
-    return $tests;
-  }
-
-}
diff --git a/web/modules/webform/tests/src/Unit/Utility/WebformElementHelperTest.php b/web/modules/webform/tests/src/Unit/Utility/WebformElementHelperTest.php
index 90cddfce27d7d6fb177652a61545a61e4afb9af3..e35ff4175e936b1bde0b77e30b22de1c7ca49791 100644
--- a/web/modules/webform/tests/src/Unit/Utility/WebformElementHelperTest.php
+++ b/web/modules/webform/tests/src/Unit/Utility/WebformElementHelperTest.php
@@ -44,7 +44,6 @@ public function providerIsTitleDisplayed() {
     $tests[] = [['#title' => ''], FALSE];
     $tests[] = [['#title' => NULL], FALSE];
     $tests[] = [['#title' => 'Test', '#title_display' => 'invisible'], FALSE];
-    $tests[] = [['#title' => 'Test', '#title_display' => 'attribute'], FALSE];
     return $tests;
   }
 
diff --git a/web/modules/webform/tests/src/Unit/Utility/WebformFormHelperTest.php b/web/modules/webform/tests/src/Unit/Utility/WebformFormHelperTest.php
index 2e9e5a7b6bbcf9e937cee999cbd5f3404701fff0..4c4a6dd0e1d198a2957c0141f858d23f4ada0e31 100644
--- a/web/modules/webform/tests/src/Unit/Utility/WebformFormHelperTest.php
+++ b/web/modules/webform/tests/src/Unit/Utility/WebformFormHelperTest.php
@@ -59,10 +59,10 @@ public function testFlattenElements() {
         ],
       ],
     ];
-    $flattenend_elements = WebformFormHelper::flattenElements($elements);
+    $flattened_elements = WebformFormHelper::flattenElements($elements);
 
     // Check flattened elements.
-    $this->assertEquals($flattenend_elements, [
+    $this->assertEquals($flattened_elements, [
       'one' => [
         '#title' => 'one',
         'two' => [
@@ -79,7 +79,7 @@ public function testFlattenElements() {
     $elements['one']['two']['#title'] .= '-UPDATED';
     $elements['one']['two']['#type'] = 'textfield';
 
-    $this->assertEquals($flattenend_elements, [
+    $this->assertEquals($flattened_elements, [
       'one' => [
         '#title' => 'one-UPDATED',
         'two' => [
@@ -105,8 +105,8 @@ public function testFlattenElements() {
         '#title' => 'two-SECOND',
       ],
     ];
-    $flattenend_elements = WebformFormHelper::flattenElements($elements);
-    $this->assertEquals($flattenend_elements, [
+    $flattened_elements = WebformFormHelper::flattenElements($elements);
+    $this->assertEquals($flattened_elements, [
       'one' => [
         '#title' => 'one',
         'two' => [
@@ -128,7 +128,7 @@ public function testFlattenElements() {
     $elements['one']['two']['#title'] .= '-UPDATED';
     $elements['one']['two']['#type'] = 'textfield';
 
-    $this->assertEquals($flattenend_elements, [
+    $this->assertEquals($flattened_elements, [
       'one' => [
         '#title' => 'one-UPDATED',
         'two' => [
diff --git a/web/modules/webform/tests/src/Unit/Utility/WebformObjectHelperTest.php b/web/modules/webform/tests/src/Unit/Utility/WebformObjectHelperTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..8bf3a389e807a8be1cd54e46f49bd0792b381efe
--- /dev/null
+++ b/web/modules/webform/tests/src/Unit/Utility/WebformObjectHelperTest.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\Tests\webform\Unit\Utility;
+
+use Drupal\webform\Utility\WebformObjectHelper;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests webform object utility.
+ *
+ * @group webform
+ *
+ * @coversDefaultClass \Drupal\webform\Utility\WebformObjectHelper
+ */
+class WebformObjectHelperTest extends UnitTestCase {
+
+  /**
+   * Tests sorting object by properties.
+   *
+   * @param object $object
+   *   The object to run through WebformObjectHelper::sortByProperty().
+   * @param array $expected
+   *   The expected result from calling the function.
+   *
+   * @see WebformObjectHelper::sortByProperty()
+   *
+   * @dataProvider providerSortByProperty
+   */
+  public function testSortByProperty($object, array $expected) {
+    $result = (array) WebformObjectHelper::sortByProperty($object);
+    $this->assertEquals(
+      implode('|', array_keys($expected)),
+      implode('|', array_keys($result))
+    );
+  }
+
+  /**
+   * Data provider for testSortByProperty().
+   *
+   * @see testSortByProperty()
+   */
+  public function providerSortByProperty() {
+    $object = new \stdClass();
+    $object->c = 'c';
+    $object->a = 'a';
+    $object->b = 'b';
+    $tests[] = [$object, ['a' => 'a', 'b' => 'b', 'c' => 'c']];
+
+    $object = new \stdClass();
+    $object->c = 'c';
+    $object->b = 'b';
+    $object->a = 'a';
+    $tests[] = [$object, ['a' => 'a', 'b' => 'b', 'c' => 'c']];
+
+    $object = new \stdClass();
+    $object->b = 'b';
+    $object->a = 'a';
+    $object->_ = '_';
+    $tests[] = [$object, ['_' => '_', 'a' => 'a', 'b' => 'b']];
+
+    return $tests;
+  }
+
+}
diff --git a/web/modules/webform/tests/src/Unit/WebformEntityAccessControlHandlerTest.php b/web/modules/webform/tests/src/Unit/WebformEntityAccessControlHandlerTest.php
index 07cbc726cde9defc5be78b259b3e5377f5ed7772..eac7c80a9f0da9218c6e787451d107fa0920f1fd 100644
--- a/web/modules/webform/tests/src/Unit/WebformEntityAccessControlHandlerTest.php
+++ b/web/modules/webform/tests/src/Unit/WebformEntityAccessControlHandlerTest.php
@@ -4,17 +4,18 @@
 
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Cache\Cache;
-use Drupal\Core\Cache\Context\CacheContextsManager;
 use Drupal\Core\Config\Entity\ConfigEntityType;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Tests\UnitTestCase;
+use Drupal\webform\WebformAccessRulesManagerInterface;
 use Drupal\webform\Plugin\WebformSourceEntityManagerInterface;
 use Drupal\webform\WebformEntityAccessControlHandler;
 use Drupal\webform\WebformInterface;
 use Drupal\webform\WebformSubmissionInterface;
 use Drupal\webform\WebformSubmissionStorageInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\RequestStack;
 
@@ -27,33 +28,43 @@
  */
 class WebformEntityAccessControlHandlerTest extends UnitTestCase {
 
+  use StringTranslationTrait;
+
+  /**
+   * The test container.
+   *
+   * @var \Symfony\Component\DependencyInjection\ContainerInterface
+   */
+  protected $container;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->container = new ContainerBuilder();
+    \Drupal::setContainer($this->container);
+
+    // Mock cache context manager and set container.
+    // @copied from \Drupal\Tests\Core\Access\AccessResultTest::setUp
+    $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE);
+    $this->container->set('cache_contexts_manager', $cache_contexts_manager);
+  }
+
   /**
    * Tests the access logic.
    *
    * @param string $operation
    *   Operation to request from ::checkAccess() method.
-   * @param array $permissions
-   *   Array of permissions to assign to a mocked account.
-   * @param array $check_access_rules
-   *   Array of access rules that should yield 'allowed' when the mocked webform
-   *   is requested ::checkAccessRules().
    * @param array $options
-   *   Array of extra options. Allowed key-value pairs are:
-   *   - is_owner: (bool) Whether the mocked user should be owner of the
-   *     webform. Defaults to FALSE.
-   *   - is_template: (bool) Whether the mocked webform should be a template.
-   *     Defaults  to FALSE.
-   *   - is_open: (bool) Whether the mocked webform should be open.
-   *     Defaults to TRUE.
-   *   - has_token: (bool) Whether the mocked webform submission should
-   *     successfully load through token in query string. Defaults to FALSE.
+   *   Array of extra options.
    * @param array $expected
-   *   Expected data from the tested class. It should have the following
-   *   structure:
-   *   - is_allowed: (bool) Whether ::isAllowed() on the return should yield
-   *     TRUE.
-   *   - cache_tags: (array) Cache tags of the return. Defaults to [].
-   *   - cache_contexts: (array) Cache contexts of the return. Defaults to [].
+   *   Expected data from the tested class.
    * @param string $assert_message
    *   Assertion message to use for this test case.
    *
@@ -61,49 +72,59 @@ class WebformEntityAccessControlHandlerTest extends UnitTestCase {
    *
    * @dataProvider providerCheckAccess
    */
-  public function testCheckAccess($operation, array $permissions, array $check_access_rules, array $options, array $expected, $assert_message = '') {
+  public function testCheckAccess($operation, array $options, array $expected, $assert_message = '') {
+    // Set $options default value.
     $options += [
-      'is_owner' => FALSE,
-      'is_template' => FALSE,
-      'is_open' => TRUE,
-      'has_token' => FALSE,
+      // What is the request path.
+      'request_path' => '',
+      // What is the request format.
+      'request_format' => 'html',
+      // Array of permissions to assign to a mocked account.
+      'permissions' => [],
+      // Array of access rules that should yield 'allowed' when the mocked
+      // access rules manager is requested ::checkWebformAccess()
+      // or ::checkWebformSubmissionAccess().
+      'access_rules' => [],
+      // Whether the mocked user should be owner of the webform.
+      'account_is_webform_owner' => FALSE,
+      // Whether the mocked webform should be a template.
+      'webform_is_template' => FALSE,
+      // Whether the mocked webform should be open.
+      'webform_is_open' => TRUE,
+      // Whether the mocked webform submission should successfully
+      // load through token in query string. Defaults to FALSE.
+      'submission_load_from_token' => FALSE,
     ];
+
+    // Set $expected default value.
     $expected += [
-      'cache_tags' => [],
-      'cache_contexts' => [],
+      // Whether ::isAllowed() on the return should yield TRUE.
+      'access_result_is_allowed' => TRUE,
+      // Cache tags of the return.
+      'access_result_cache_tags' => [],
+      // Cache contexts of the return.
+      'access_result_cache_contexts' => [],
     ];
 
-    $cache_contexts_manager = $this->getMockBuilder(CacheContextsManager::class)
-      ->disableOriginalConstructor()
-      ->getMock();
-    $cache_contexts_manager->method('assertValidTokens')
-      ->willReturn(TRUE);
+    /**************************************************************************/
 
-    $container = $this->getMockBuilder(ContainerInterface::class)
-      ->disableOriginalConstructor()
-      ->getMock();
-    $container->method('get')
-      ->willReturnMap([
-        ['cache_contexts_manager', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $cache_contexts_manager],
-      ]);
+    $token = $this->randomMachineName();
 
-    \Drupal::setContainer($container);
-
-    $entity_type = new ConfigEntityType([
-      'id' => 'webform',
-    ]);
+    // Mock entity type.
+    $entity_type = new ConfigEntityType(['id' => 'webform']);
 
+    // Mock request stack.
     $request_stack = $this->getMockBuilder(RequestStack::class)
       ->disableOriginalConstructor()
       ->getMock();
     $request_stack->method('getCurrentRequest')
-      ->willReturn(new Request([
-        'token' => $this->randomMachineName(),
-      ]));
+      ->willReturn(new Request(['token' => $token], [], ['_format' => $options['request_format']]));
 
+    // Mock webform submission storage.
     $webform_submission_storage = $this->getMockBuilder(WebformSubmissionStorageInterface::class)
       ->getMock();
 
+    // Mock entity type manager.
     $entity_type_manager = $this->getMockBuilder(EntityTypeManagerInterface::class)
       ->getMock();
     $entity_type_manager->method('getStorage')
@@ -111,33 +132,50 @@ public function testCheckAccess($operation, array $permissions, array $check_acc
         ['webform_submission', $webform_submission_storage],
       ]);
 
+    // Mock webform source entity manager.
     $webform_source_entity_manager = $this->getMockBuilder(WebformSourceEntityManagerInterface::class)
       ->getMock();
     $webform_source_entity_manager->method('getSourceEntity')
       ->willReturn(NULL);
 
-    $access_handler = new WebformEntityAccessControlHandler($entity_type, $request_stack, $entity_type_manager, $webform_source_entity_manager);
-
+    // Mock account.
+    $permissions = $options['permissions'];
+    $account_id = 2;
     $account = $this->getMockBuilder(AccountInterface::class)
       ->getMock();
     $account->method('hasPermission')
       ->willReturnCallback(function ($permission) use ($permissions) {
         return in_array($permission, $permissions);
       });
+    $account->method('id')
+      ->willReturn($options['account_is_webform_owner'] ? $account_id : $account_id + 1);
+    $account->method('isAuthenticated')
+      ->willReturn($account->id() > 0);
 
+    // Mock webform.
     $webform = $this->getMockBuilder(WebformInterface::class)
       ->getMock();
     $webform->method('getOwnerId')
-      ->willReturn(2);
+      ->willReturn($account_id);
     $webform->method('isTemplate')
-      ->willReturn($options['is_template']);
+      ->willReturn($options['webform_is_template']);
     $webform->method('isOpen')
-      ->willReturn($options['is_open']);
+      ->willReturn($options['webform_is_open']);
     $webform->method('access')
       ->willReturnMap([
-        ['create', $account, TRUE, AccessResult::forbidden()],
+        ['create', $account, TRUE, AccessResult::allowed()],
       ]);
+    $webform->method('getSetting')->willReturnMap([
+      ['page', FALSE, TRUE],
+    ]);
+    $webform->method('getCacheMaxAge')
+      ->willReturn(Cache::PERMANENT);
+    $webform->method('getCacheContexts')
+      ->willReturn(['webform_cache_context']);
+    $webform->method('getCacheTags')
+      ->willReturn(['webform_cache_tag']);
 
+    // Mock webform submissions.
     $webform_submission = $this->getMockBuilder(WebformSubmissionInterface::class)
       ->getMock();
     $webform_submission->method('getCacheContexts')
@@ -146,35 +184,41 @@ public function testCheckAccess($operation, array $permissions, array $check_acc
       ->willReturn(['webform_submission_cache_tag']);
     $webform_submission->method('getCacheMaxAge')
       ->willReturn(Cache::PERMANENT);
+    $webform_submission->method('getWebform')
+      ->willReturn($webform);
     $webform_submission_storage->method('loadFromToken')
       ->willReturnMap([
-        [$request_stack->getCurrentRequest()->query->get('token'), $webform, $webform_source_entity_manager->getSourceEntity(['webform']), NULL, ($options['has_token'] ? $webform_submission : NULL)],
+        [$token, $webform, NULL, NULL, ($options['submission_load_from_token'] ? $webform_submission : NULL)],
       ]);
 
-    $check_access_rules_map = [];
-    foreach (['administer', 'page', 'view_any', 'view_own'] as $v) {
-      $check_access_rules_map[] = [$v, $account, NULL, AccessResult::allowedIf(in_array($v, $check_access_rules))->addCacheContexts(['check_access_rules_cache_context'])->addCacheTags(['check_access_rules_cache_tag'])];
-    }
-    $webform->method('checkAccessRules')
-      ->willReturnMap($check_access_rules_map);
-    $webform->method('getCacheMaxAge')
-      ->willReturn(Cache::PERMANENT);
-    $webform->method('getCacheContexts')
-      ->willReturn(['webform_cache_context']);
-    $webform->method('getCacheTags')
-      ->willReturn(['webform_cache_tag']);
-
-    $account->method('id')
-      ->willReturn($options['is_owner'] ? $webform->getOwnerId() : $webform->getOwnerId() + 1);
-    $account->method('isAuthenticated')
-      ->willReturn($account->id() > 0);
-
+    // Mock access rules manager.
+    $access_rules_manager = $this->getMockBuilder(WebformAccessRulesManagerInterface::class)
+      ->getMock();
+    $access_rules_manager->method('checkWebformAccess')
+      ->will(
+        $this->returnCallback(
+          function ($operation, AccountInterface $account, WebformInterface $webform) use ($options) {
+            $condition = in_array($operation, $options['access_rules']) || in_array($operation . '_any', $options['access_rules']);
+            return AccessResult::allowedIf($condition)
+              ->addCacheContexts(['access_rules_cache_context'])
+              ->addCacheTags(['access_rules_cache_tag']);
+          }
+        )
+      );
+
+    /**************************************************************************/
+
+    // Create webform access control handler.
+    $access_handler = new WebformEntityAccessControlHandler($entity_type, $request_stack, $entity_type_manager, $webform_source_entity_manager, $access_rules_manager);
+
+    // Check access.
     $access_result = $access_handler->checkAccess($webform, $operation, $account);
 
-    $this->assertEquals($expected['is_allowed'], $access_result->isAllowed(), $assert_message);
+    // Check expected results.
+    $this->assertEquals($expected['access_result_is_allowed'], $access_result->isAllowed(), $assert_message);
     $this->assertEquals(Cache::PERMANENT, $access_result->getCacheMaxAge(), $assert_message . ': cache max age');
-    $this->assertArrayEquals($expected['cache_contexts'], $access_result->getCacheContexts(), $assert_message . ': cache contexts');
-    $this->assertArrayEquals($expected['cache_tags'], $access_result->getCacheTags(), $assert_message . ': cache tags');
+    $this->assertArrayEquals($expected['access_result_cache_contexts'], $access_result->getCacheContexts(), $assert_message . ': cache contexts');
+    $this->assertArrayEquals($expected['access_result_cache_tags'], $access_result->getCacheTags(), $assert_message . ': cache tags');
   }
 
   /**
@@ -183,164 +227,624 @@ public function testCheckAccess($operation, array $permissions, array $check_acc
    * @see testCheckAccess()
    */
   public function providerCheckAccess() {
-    $tests[] = ['view', [], [], [], ['is_allowed' => TRUE], 'View operation'];
+    $tests = [];
+
+    /**************************************************************************/
+    // The "view" HTML operation.
+    /**************************************************************************/
+
+    $tests[] = [
+      'view',
+      [],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['access_rules_cache_tag'],
+        'access_result_cache_contexts' => ['access_rules_cache_context', 'request_format', 'url.path'],
+      ],
+      'View when nobody',
+    ];
+
+    $tests[] = [
+      'view',
+      [
+        'permissions' => ['administer webform'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => [],
+        'access_result_cache_contexts' => ['user.permissions'],
+      ],
+      'View when has "administer webform" permission',
+    ];
 
+    $tests[] = [
+      'view',
+      [
+        'access_rules' => ['administer'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['access_rules_cache_tag'],
+        'access_result_cache_contexts' => ['access_rules_cache_context'],
+      ],
+      'View when has "administer" access rule',
+    ];
+
+    /**************************************************************************/
+    // The "view" configuration operation.
+    /**************************************************************************/
+
+    $tests[] = [
+      'view',
+      [
+        'permissions' => ['access any webform configuration'],
+      ],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['access_rules_cache_tag'],
+        'access_result_cache_contexts' => ['access_rules_cache_context', 'request_format', 'url.path'],
+      ],
+      'View when has "access any webform configuration" permission and request form is HTML',
+    ];
+
+    $tests[] = [
+      'view',
+      [
+        'request_format' => 'not_html',
+        'permissions' => ['access any webform configuration'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['request_format', 'url.path', 'user', 'user.permissions', 'webform_cache_context'],
+      ],
+      'View when has "access any webform configuration" permission and request form is NOT HTML',
+    ];
+
+    $tests[] = [
+      'view',
+      [
+        'permissions' => ['access own webform configuration'],
+        'account_is_webform_owner' => FALSE,
+      ],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['access_rules_cache_tag'],
+        'access_result_cache_contexts' => ['access_rules_cache_context', 'request_format', 'url.path'],
+      ],
+      'View when has "access own webform configuration" permission and is not owner and request form is HTML',
+    ];
+
+    $tests[] = [
+      'view',
+      [
+        'request_format' => 'not_html',
+        'permissions' => ['access own webform configuration'],
+        'account_is_webform_owner' => FALSE,
+      ],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['access_rules_cache_tag'],
+        'access_result_cache_contexts' => ['access_rules_cache_context', 'request_format', 'url.path'],
+      ],
+      'View when has "access own webform configuration" permission and is not owner and request form is NOT HTML',
+    ];
+
+    $tests[] = [
+      'view',
+      [
+        'permissions' => ['access own webform configuration'],
+        'account_is_webform_owner' => TRUE,
+      ],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['access_rules_cache_tag'],
+        'access_result_cache_contexts' => ['access_rules_cache_context', 'request_format', 'url.path'],
+      ],
+      'View when has "access own webform configuration" permission and is owner and request form is HTML',
+    ];
+
+    $tests[] = [
+      'view',
+      [
+        'request_format' => 'not_html',
+        'permissions' => ['access own webform configuration'],
+        'account_is_webform_owner' => TRUE,
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['request_format', 'url.path', 'user', 'user.permissions', 'webform_cache_context'],
+      ],
+      'View when has "access own webform configuration" permission and is owner and request form is NOT HTML',
+    ];
+
+    /**************************************************************************/
+    // The "test" operation.
+    /**************************************************************************/
+
+    $tests[] = [
+      'test',
+      [],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Test when nobody',
+    ];
+
+    $tests[] = [
+      'test',
+      [
+        'permissions' => ['administer webform'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => [],
+        'access_result_cache_contexts' => ['user.permissions'],
+      ],
+      'Test when has "administer webform" permission',
+    ];
+
+    $tests[] = [
+      'test',
+      [
+        'access_rules' => ['administer'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['access_rules_cache_tag'],
+        'access_result_cache_contexts' => ['access_rules_cache_context'],
+      ],
+      'Test when has "administer" access rule',
+    ];
+
+    $tests[] = [
+      'test',
+      [
+        'permissions' => ['edit any webform'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user', 'user.permissions', 'webform_cache_context'],
+      ],
+      'Test when has "edit any webform" permission',
+    ];
+
+    $tests[] = [
+      'test',
+      [
+        'permissions' => ['edit own webform'],
+        'account_is_webform_owner' => FALSE,
+      ],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Test when has "edit own webform" permission and is not owner',
+    ];
+
+    $tests[] = [
+      'test',
+      [
+        'permissions' => ['edit own webform'],
+        'account_is_webform_owner' => TRUE,
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user', 'user.permissions', 'webform_cache_context'],
+      ],
+      'Test when has "edit own webform" permission and is owner',
+    ];
+
+    /**************************************************************************/
     // The "update" operation.
-    $tests[] = ['update', [], [], [], [
-      'is_allowed' => FALSE,
-      'cache_tags' => ['webform_cache_tag'],
-      'cache_contexts' => ['webform_cache_context'],
-    ], 'Update when nobody'];
-
-    $tests[] = ['update', [], ['administer'], [], [
-      'is_allowed' => TRUE,
-      'cache_tags' => ['check_access_rules_cache_tag', 'webform_cache_tag'],
-      'cache_contexts' => ['check_access_rules_cache_context', 'user', 'user.permissions', 'webform_cache_context'],
-    ], 'Update when admin of the webform'];
-
-    $tests[] = ['update', ['edit any webform'], [], [], [
-      'is_allowed' => TRUE,
-      'cache_tags' => ['check_access_rules_cache_tag', 'webform_cache_tag'],
-      'cache_contexts' => ['check_access_rules_cache_context', 'user', 'user.permissions', 'webform_cache_context'],
-    ], 'Update when has "edit any webform" permission'];
-
-    $tests[] = ['update', ['edit own webform'], [], [], [
-      'is_allowed' => FALSE,
-      'cache_tags' => ['webform_cache_tag'],
-      'cache_contexts' => ['webform_cache_context'],
-    ], 'Update when has "edit own webform" permission but is not owner'];
-
-    $tests[] = ['update', ['edit own webform'], [], ['is_owner' => TRUE], [
-      'is_allowed' => TRUE,
-      'cache_tags' => ['check_access_rules_cache_tag', 'webform_cache_tag'],
-      'cache_contexts' => ['check_access_rules_cache_context', 'user', 'user.permissions', 'webform_cache_context'],
-    ], 'Update when has "edit own webform" permission and is owner'];
+    /**************************************************************************/
+
+    $tests[] = [
+      'update',
+      [],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Update when nobody',
+    ];
+
+    $tests[] = [
+      'update',
+      [
+        'permissions' => ['administer webform'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => [],
+        'access_result_cache_contexts' => ['user.permissions'],
+      ],
+      'Update when has "administer webform" permission',
+    ];
+
+    $tests[] = [
+      'update', [
+        'access_rules' => ['administer'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['access_rules_cache_tag'],
+        'access_result_cache_contexts' => ['access_rules_cache_context'],
+      ],
+      'Update when has "administer" access rule',
+    ];
+
+    $tests[] = [
+      'update',
+      [
+        'permissions' => ['edit any webform'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user', 'user.permissions', 'webform_cache_context'],
+      ],
+      'Update when has "edit any webform" permission',
+    ];
 
+    $tests[] = [
+      'update',
+      [
+        'permission' => ['edit own webform'],
+        'account_is_webform_owner' => FALSE,
+      ],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Update when has "edit own webform" permission and is not owner',
+    ];
+
+    $tests[] = [
+      'update', [
+        'permissions' => ['edit own webform'],
+        'account_is_webform_owner' => TRUE,
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user', 'user.permissions', 'webform_cache_context'],
+      ],
+      'Update when has "edit own webform" permission and is owner',
+    ];
+
+    /**************************************************************************/
     // The "duplicate" operation.
-    $tests[] = ['duplicate', [], [], [], [
-      'is_allowed' => FALSE,
-      'cache_tags' => ['webform_cache_tag'],
-      'cache_contexts' => ['webform_cache_context'],
-    ], 'Duplicate when nobody'];
-
-    $tests[] = ['duplicate', [], ['administer'], [], [
-      'is_allowed' => TRUE,
-      'cache_tags' => ['check_access_rules_cache_tag', 'webform_cache_tag'],
-      'cache_contexts' => ['check_access_rules_cache_context', 'user', 'user.permissions', 'webform_cache_context'],
-    ], 'Duplicate when admin of the webform'];
-
-    $tests[] = ['duplicate', ['create webform'], [], ['is_template' => TRUE], [
-      'is_allowed' => TRUE,
-      'cache_tags' => ['check_access_rules_cache_tag', 'webform_cache_tag'],
-      'cache_contexts' => ['check_access_rules_cache_context', 'user', 'user.permissions', 'webform_cache_context'],
-    ], 'Duplicate when has "create webform" and the webform is a template'];
-
-    $tests[] = ['duplicate', ['create webform', 'edit any webform'], [], [], [
-      'is_allowed' => TRUE,
-      'cache_tags' => ['check_access_rules_cache_tag', 'webform_cache_tag'],
-      'cache_contexts' => ['check_access_rules_cache_context', 'user', 'user.permissions', 'webform_cache_context'],
-    ], 'Duplicate when has "create webform" and "edit any webform"'];
-
-    $tests[] = ['duplicate', ['create webform', 'edit own webform'], [], [], [
-      'is_allowed' => FALSE,
-      'cache_tags' => ['webform_cache_tag'],
-      'cache_contexts' => ['webform_cache_context'],
-    ], 'Duplicate when has "create webform" and "edit own webform" but is not owner'];
-
-    $tests[] = ['duplicate', ['create webform', 'edit own webform'], [], ['is_owner' => TRUE], [
-      'is_allowed' => TRUE,
-      'cache_tags' => ['check_access_rules_cache_tag', 'webform_cache_tag'],
-      'cache_contexts' => ['check_access_rules_cache_context', 'user', 'user.permissions', 'webform_cache_context'],
-    ], 'Duplicate when has "create webform" and "edit own webform" and is owner'];
+    /**************************************************************************/
+
+    $tests[] = [
+      'duplicate',
+      [],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Duplicate when nobody',
+    ];
+
+    $tests[] = [
+      'duplicate',
+      [
+        'permissions' => ['administer webform'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => [],
+        'access_result_cache_contexts' => ['user.permissions'],
+      ],
+      'Duplicate when has "administer webform" permission',
+    ];
+
+    $tests[] = [
+      'duplicate',
+      [
+        'access_rules' => ['administer'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['access_rules_cache_tag'],
+        'access_result_cache_contexts' => ['access_rules_cache_context'],
+      ],
+      'Duplicate when has "administer" access rule',
+    ];
+
+    $tests[] = [
+      'duplicate',
+      [
+        'permissions' => ['create webform'],
+        'webform_is_template' => FALSE,
+      ],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Duplicate when has "create webform" permission and webform is not template',
+    ];
+
+    $tests[] = [
+      'duplicate',
+      [
+        'permissions' => ['create webform', 'edit any webform'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user', 'user.permissions', 'webform_cache_context'],
+      ],
+      'Duplicate when has "create webform" and "edit any webform" permissions',
+    ];
+
+    $tests[] = [
+      'duplicate', [
+        'permisssions' => ['create webform', 'edit own webform'],
+        'account_is_webform_owner' => FALSE,
+      ],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Duplicate when has "create webform" and "edit own webform" permissions and is not owner',
+    ];
+
+    $tests[] = [
+      'duplicate',
+      [
+        'permissions' => ['create webform', 'edit own webform'],
+        'account_is_webform_owner' => TRUE,
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user', 'user.permissions', 'webform_cache_context'],
+      ],
+      'Duplicate when has "create webform" and "edit own webform" permissions and is owner',
+    ];
 
+    /**************************************************************************/
     // The "delete" operation.
-    $tests[] = ['delete', [], [], [], [
-      'is_allowed' => FALSE,
-      'cache_tags' => ['webform_cache_tag'],
-      'cache_contexts' => ['webform_cache_context'],
-    ], 'Delete when nobody'];
-
-    $tests[] = ['delete', [], ['administer'], [], [
-      'is_allowed' => TRUE,
-      'cache_tags' => ['check_access_rules_cache_tag', 'webform_cache_tag'],
-      'cache_contexts' => ['check_access_rules_cache_context', 'user', 'user.permissions', 'webform_cache_context'],
-    ], 'Delete when admin of the webform'];
-    $tests[] = ['delete', ['delete any webform'], [], [], [
-      'is_allowed' => TRUE,
-      'cache_tags' => ['check_access_rules_cache_tag', 'webform_cache_tag'],
-      'cache_contexts' => ['check_access_rules_cache_context', 'user', 'user.permissions', 'webform_cache_context'],
-    ], 'Delete when has "delete any webform"'];
-
-    $tests[] = ['delete', ['delete own webform'], [], [], [
-      'is_allowed' => FALSE,
-      'cache_tags' => ['webform_cache_tag'],
-      'cache_contexts' => ['webform_cache_context'],
-    ], 'Delete when has "delete own webform" but is not owner'];
-
-    $tests[] = ['delete', ['delete own webform'], [], ['is_owner' => TRUE], [
-      'is_allowed' => TRUE,
-      'cache_tags' => ['check_access_rules_cache_tag', 'webform_cache_tag'],
-      'cache_contexts' => ['check_access_rules_cache_context', 'user', 'user.permissions', 'webform_cache_context'],
-    ], 'Delete when has "delete own webform" and is owner'];
-
-    $tests[] = ['submission_view_any', [], [], [], [
-      'is_allowed' => FALSE,
-      'cache_tags' => ['webform_cache_tag'],
-      'cache_contexts' => ['webform_cache_context'],
-    ], 'Submission view any when nobody'];
-
-    $tests[] = ['submission_view_any', ['view any webform submission'], [], [], [
-      'is_allowed' => TRUE,
-      'cache_contexts' => ['user.permissions'],
-    ], 'Submission view any when has "view any webform submission" permission'];
-
-    $tests[] = ['submission_view_any', ['view own webform submission'], [], [], [
-      'is_allowed' => FALSE,
-      'cache_tags' => ['webform_cache_tag'],
-      'cache_contexts' => ['webform_cache_context'],
-    ], 'Submission view any when has "view own webform submission" permission but is not owner'];
-
-    $tests[] = ['submission_view_any', ['view own webform submission'], [], ['is_owner' => TRUE], [
-      'is_allowed' => TRUE,
-      'cache_tags' => ['webform_cache_tag'],
-      'cache_contexts' => ['user', 'webform_cache_context'],
-    ], 'Submission view any when has "view own webform submission" permission and is owner'];
-
-    $tests[] = ['submission_view_own', [], [], [], [
-      'is_allowed' => FALSE,
-      'cache_tags' => ['webform_cache_tag'],
-      'cache_contexts' => ['webform_cache_context'],
-    ], 'Submission view own when nobody'];
-
-    $tests[] = ['submission_view_own', ['view own webform submission'], [], [], [
-      'is_allowed' => TRUE,
-      'cache_contexts' => ['user.permissions'],
-    ], 'Submission view own when has "view own webform submission" permission'];
+    /**************************************************************************/
+
+    $tests[] = [
+      'delete',
+      [],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Delete when nobody',
+    ];
+
+    $tests[] = [
+      'delete',
+      [
+        'permissions' => ['administer webform'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => [],
+        'access_result_cache_contexts' => ['user.permissions'],
+      ],
+      'Delete when has "administer webform" permission',
+    ];
+
+    $tests[] = [
+      'delete',
+      [
+        'access_rules' => ['administer'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['access_rules_cache_tag'],
+        'access_result_cache_contexts' => ['access_rules_cache_context'],
+      ],
+      'Delete when has "administer" access rule',
+    ];
+
+    $tests[] = [
+      'delete',
+      [
+        'permissions' => ['delete any webform'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user', 'user.permissions', 'webform_cache_context'],
+      ],
+      'Delete when has "delete any webform" permission',
+    ];
+
+    $tests[] = [
+      'delete',
+      [
+        'permissions' => ['delete own webform'],
+        'account_is_webform_owner' => FALSE,
+      ],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Delete when has "delete own webform" permission and is not owner',
+    ];
+
+    $tests[] = [
+      'delete',
+      [
+        'permissions' => ['delete own webform'],
+        'account_is_webform_owner' => TRUE,
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user', 'user.permissions', 'webform_cache_context'],
+      ],
+      'Delete when has "delete own webform" permission and is owner',
+    ];
+
+    /**************************************************************************/
+    // The "purge" operation.
+    /**************************************************************************/
+
+    $tests[] = [
+      'submission_purge',
+      [
+        'access_rules' => ['purge_any'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['access_rules_cache_tag'],
+        'access_result_cache_contexts' => ['access_rules_cache_context'],
+      ],
+      'Purge when has "purge_any" access rule',
+    ];
+
+    $tests[] = [
+      'submission_purge_any',
+      [
+        'access_rules' => ['purge_any'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['access_rules_cache_tag'],
+        'access_result_cache_contexts' => ['access_rules_cache_context'],
+      ],
+      'Purge when has "purge_any" access rule',
+    ];
+
+    /**************************************************************************/
+    // The "view" operation.
+    /**************************************************************************/
+
+    $tests[] = [
+      'submission_view_any',
+      [],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Submission view any when nobody',
+    ];
+
+    $tests[] = [
+      'submission_view_any',
+      [
+        'permissions' => ['view any webform submission'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_contexts' => ['user.permissions'],
+      ],
+      'Submission view any when has "view any webform submission" permission',
+    ];
+
+    $tests[] = [
+      'submission_view_any',
+      [
+        'permissions' => ['view own webform submission'],
+      ],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Submission view any when has "view own webform submission" permission but is not owner',
+    ];
+
+    $tests[] = [
+      'submission_view_own',
+      [],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Submission view own when nobody',
+    ];
+
+    $tests[] = [
+      'submission_view_own',
+      [
+        'permissions' => ['view own webform submission'],
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_contexts' => ['user.permissions'],
+      ],
+      'Submission view own when has "view own webform submission" permission',
+    ];
 
     // The "submission_page" operation.
-    $tests[] = ['submission_page', [], [], ['is_open' => FALSE], [
-      'is_allowed' => FALSE,
-      'cache_tags' => ['webform_cache_tag'],
-      'cache_contexts' => ['webform_cache_context'],
-    ], 'Submission page when nobody'];
-
-    $tests[] = ['submission_page', [], [], ['has_token' => TRUE], [
-      'is_allowed' => TRUE,
-      'cache_tags' => ['webform_cache_tag', 'webform_submission_cache_tag'],
-      'cache_contexts' => ['url', 'webform_cache_context', 'webform_submission_cache_context'],
-    ], 'Submission page when accessible through token'];
-
-    $tests[] = ['submission_page', [], [], ['is_template' => TRUE, 'is_open' => FALSE], [
-      'is_allowed' => FALSE,
-      'cache_tags' => ['webform_cache_tag'],
-      'cache_contexts' => ['webform_cache_context'],
-    ], 'Submission page when the webform is template without create access'];
-
-    $tests[] = ['submission_page', [], ['page'], ['is_open' => FALSE], [
-      'is_allowed' => TRUE,
-      'cache_tags' => ['check_access_rules_cache_tag', 'webform_cache_tag'],
-      'cache_contexts' => ['check_access_rules_cache_context', 'webform_cache_context'],
-    ], 'Submission page when the webform allows "page"'];
+    $tests[] = [
+      'submission_page',
+      [
+        'webform_is_open' => FALSE,
+      ],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Submission page when nobody',
+    ];
+
+    $tests[] = [
+      'submission_page',
+      [
+        'submission_load_from_token' => TRUE,
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['webform_cache_tag', 'webform_submission_cache_tag'],
+        'access_result_cache_contexts' => ['url', 'user.permissions', 'webform_cache_context', 'webform_submission_cache_context'],
+      ],
+      'Submission page when accessible through token',
+    ];
+
+    $tests[] = [
+      'submission_page',
+      [
+        'webform_is_template' => TRUE,
+        'webform_is_open' => FALSE,
+      ],
+      [
+        'access_result_is_allowed' => FALSE,
+        'access_result_cache_tags' => ['webform_cache_tag'],
+        'access_result_cache_contexts' => ['user.permissions', 'webform_cache_context'],
+      ],
+      'Submission page when the webform is template without create access',
+    ];
+
+    $tests[] = [
+      'submission_page',
+      [
+        'access_rules' => ['create'],
+        'webform_is_open' => FALSE,
+      ],
+      [
+        'access_result_is_allowed' => TRUE,
+        'access_result_cache_tags' => ['access_rules_cache_tag'],
+        'access_result_cache_contexts' => ['access_rules_cache_context'],
+      ],
+      'Submission page when the webform allows "page"',
+    ];
 
     return $tests;
   }
diff --git a/web/modules/webform/tests/themes/webform_test_bartik/templates/webform/webform-email-message-html--contact--email--email-confirmation.html.twig b/web/modules/webform/tests/themes/webform_test_bartik/templates/webform/webform-email-message-html--contact--email--email-confirmation.html.twig
index f035c2e306aa3de97d3a17dfd855881ecbd1d30b..9b6c7b73b505e0ef5ab2f762603266c8432f5d32 100644
--- a/web/modules/webform/tests/themes/webform_test_bartik/templates/webform/webform-email-message-html--contact--email--email-confirmation.html.twig
+++ b/web/modules/webform/tests/themes/webform_test_bartik/templates/webform/webform-email-message-html--contact--email--email-confirmation.html.twig
@@ -4,7 +4,7 @@
  * Default theme implementation for webform email wrapper template as HTML.
  *
  * Available variables:
- * - message: The email message which contains to, from, subject, message, etc...
+ * - message: The email message which contains to, from, subject, message, etc…
  * - webform_submission: The webform submission.
  * - handler: The webform handler.
  *
diff --git a/web/modules/webform/tests/themes/webform_test_bartik/templates/webform/webform-email-message-html.html.twig b/web/modules/webform/tests/themes/webform_test_bartik/templates/webform/webform-email-message-html.html.twig
index 3e4139da0f22c0cebd99563ec244f23d7478b6f3..773fcb8932ec36ff7c782edeb210d2be2edd53b1 100644
--- a/web/modules/webform/tests/themes/webform_test_bartik/templates/webform/webform-email-message-html.html.twig
+++ b/web/modules/webform/tests/themes/webform_test_bartik/templates/webform/webform-email-message-html.html.twig
@@ -4,7 +4,7 @@
  * Default theme implementation for webform email wrapper template as HTML.
  *
  * Available variables:
- * - message: The email message which contains to, from, subject, message, etc...
+ * - message: The email message which contains to, from, subject, message, etc…
  * - webform_submission: The webform submission.
  * - handler: The webform handler.
  *
diff --git a/web/modules/webform/tests/themes/webform_test_bartik/webform_test_bartik.info.yml b/web/modules/webform/tests/themes/webform_test_bartik/webform_test_bartik.info.yml
index 3ac295b470d22f23760f0c3a7c7f76267a13d21f..decb95e2a9ef51933e2f20b4314162358eb9ac08 100644
--- a/web/modules/webform/tests/themes/webform_test_bartik/webform_test_bartik.info.yml
+++ b/web/modules/webform/tests/themes/webform_test_bartik/webform_test_bartik.info.yml
@@ -5,8 +5,8 @@ name: 'Webform Test Bartik'
 description: 'Test Webform Bartik integration.'
 package: 'Webform Test'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/third_party_settings/webform.antibot.inc b/web/modules/webform/third_party_settings/webform.antibot.inc
index 2cb5cbf2b54b2f85c79336dc751fa250c7937551..040085561d936d6eceeb523c26c8566c6b80abc1 100644
--- a/web/modules/webform/third_party_settings/webform.antibot.inc
+++ b/web/modules/webform/third_party_settings/webform.antibot.inc
@@ -137,7 +137,7 @@ function antibot_webform_third_party_settings_form_alter(&$form, FormStateInterf
  */
 function antibot_webform_submission_form_alter(&$form, FormStateInterface $form_state, $form_id) {
   // Only add an Antibot when a webform is initially load.
-  // After a webform is submitted, via a multistep webform and/or saving a draft,
+  // After a webform is submitted, via a multi-step webform and/or saving a draft,
   // we can skip adding an Antibot.
   if ($form_state->isSubmitted()) {
     return;
diff --git a/web/modules/webform/third_party_settings/webform.captcha.inc b/web/modules/webform/third_party_settings/webform.captcha.inc
new file mode 100644
index 0000000000000000000000000000000000000000..f2b6ff388e5a0bef24168f7dde0dde945dc2d6ad
--- /dev/null
+++ b/web/modules/webform/third_party_settings/webform.captcha.inc
@@ -0,0 +1,148 @@
+<?php
+
+/**
+ * @file
+ * Integrates third party settings on the CAPTCHA module's behalf.
+ */
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\webform\Utility\WebformElementHelper;
+
+/**
+ * Implements hook_webform_admin_third_party_settings_form_alter().
+ */
+function captcha_webform_admin_third_party_settings_form_alter(&$form, FormStateInterface $form_state) {
+  /** @var \Drupal\webform\WebformThirdPartySettingsManagerInterface $third_party_settings_manager */
+  $third_party_settings_manager = \Drupal::service('webform.third_party_settings_manager');
+  $replace_administration_mode = $third_party_settings_manager->getThirdPartySetting('captcha', 'replace_administration_mode');
+
+  $t_args = [
+    ':href' => Url::fromRoute('captcha_settings')->toString(),
+  ];
+
+  $form['third_party_settings']['captcha'] = [
+    '#type' => 'details',
+    '#title' => t('CAPTCHA'),
+    '#open' => TRUE,
+  ];
+  $form['third_party_settings']['captcha']['replace_administration_mode'] = [
+    '#type' => 'checkbox',
+    '#title' => t('Replace <a href=":href">Add CAPTCHA administration links to forms</a> with CAPTCHA webform element', $t_args),
+    '#default_value' => $replace_administration_mode,
+    '#return_value' => TRUE,
+  ];
+}
+
+/**
+ * Implements hook_webform_submission_form_alter().
+ */
+function captcha_webform_submission_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  // Make sure CAPTCHA admin mode is enabled and available to the current user.
+  $config = \Drupal::config('captcha.settings');
+  if (!$config->get('administration_mode')
+    || !\Drupal::currentUser()->hasPermission('administer CAPTCHA settings')) {
+    return;
+  }
+
+  // Make sure the CAPTCHA webform element is enabled.
+  /** @var \Drupal\webform\Plugin\WebformElementManagerInterface $element_manager */
+  $element_manager = \Drupal::service('plugin.manager.webform.element');
+  if ($element_manager->isExcluded('captcha')) {
+    return;
+  }
+
+  // Make sure replace administrative mode is enabled.
+  /** @var \Drupal\webform\WebformThirdPartySettingsManagerInterface $third_party_settings_manager */
+  $third_party_settings_manager = \Drupal::service('webform.third_party_settings_manager');
+  $replace_administration_mode = $third_party_settings_manager->getThirdPartySetting('captcha', 'replace_administration_mode');
+  if (!$replace_administration_mode) {
+    return;
+  }
+
+  // If the webform already has a CAPTCHA point is alread configured, do not
+  // do anything.
+  /* @var \Drupal\captcha\CaptchaPointInterface $captcha_point */
+  $captcha_point = \Drupal::entityTypeManager()
+    ->getStorage('captcha_point')
+    ->load($form_id);
+  if ($captcha_point) {
+    return;
+  }
+
+  $form['#after_build'][] = '_captcha_webform_submission_form_after_build';
+}
+
+/**
+ * After buld callback to add warning to CAPTCHA placement.
+ */
+function _captcha_webform_submission_form_after_build(array $form, FormStateInterface $form_state) {
+  // Make sure 'Add CAPTCHA administration links to forms' is appended to the
+  // webform.
+  // @see /admin/config/people/captcha
+  // @see captcha_form_alter()
+  if (!isset($form['captcha']) || !isset($form['captcha']['add_captcha'])) {
+    return $form;
+  }
+
+  /** @var \Drupal\webform\WebformSubmissionForm $form_object */
+  $form_object = $form_state->getFormObject();
+  $webform = $form_object->getWebform();
+
+  // Determine if the current user can update this webform via the UI.
+  $has_update_access = $webform->access('update')
+    && \Drupal::moduleHandler()->moduleExists('webform_ui');
+
+  // If the webform has a CAPTCHA element, display a link to edit the element.
+  $elements = $webform->getElementsInitializedAndFlattened();
+  foreach ($elements as $key => $element) {
+    if (WebformElementHelper::isType($element, 'captcha')) {
+      // Update the details element's title.
+      $form['captcha']['#title'] = t('CAPTCHA: challenge enabled');
+
+      // Replace 'Place a CAPTCHA here for untrusted users' link with link to
+      // edit CAPTCHA element for this webform.
+      if ($has_update_access) {
+        $route_name = 'entity.webform_ui.element.edit_form';
+        $route_parameters = ['webform' => $webform->id(), 'key' => $key];
+        $route_options = ['query' => Drupal::destination()->getAsArray()];
+        $form['captcha']['add_captcha'] = [
+          '#type' => 'link',
+          '#title' => t('Untrusted users will see a CAPTCHA element on this webform.'),
+          '#url' => Url::fromRoute($route_name, $route_parameters, $route_options),
+          '#prefix' => '<em>',
+          '#suffix' => '</em>',
+        ];
+      }
+      else {
+        $form['captcha']['add_captcha'] = [
+          '#markup' => t('Untrusted users will see a CAPTCHA element on this webform.'),
+          '#prefix' => '<em>',
+          '#suffix' => '</em>',
+        ];
+      }
+      return $form;
+    }
+  }
+
+  // Replace 'Place a CAPTCHA here for untrusted users' link with link to
+  // add CAPTCHA element to this webform.
+  if ($has_update_access) {
+    $route_name = 'entity.webform_ui.element.add_form';
+    $route_parameters = ['webform' => $webform->id(), 'type' => 'captcha'];
+    $route_options = ['query' => Drupal::destination()->getAsArray()];
+    $form['captcha']['add_captcha'] = [
+      '#type' => 'link',
+      '#title' => t('Add CAPTCHA element to this webform for untrusted users.'),
+      '#url' => Url::fromRoute($route_name, $route_parameters, $route_options),
+    ];
+  }
+  else {
+    $form['captcha']['add_captcha'] = [
+      '#type' => 'webform_message',
+      '#message_message' => t('CAPTCHA should be added as an element to this webform.'),
+      '#message_type' => 'warning',
+    ];
+  }
+  return $form;
+}
diff --git a/web/modules/webform/third_party_settings/webform.honeypot.inc b/web/modules/webform/third_party_settings/webform.honeypot.inc
index 89d106b9e5d060bdd9387095acb58c017ed44f70..a97529676d2a145c6530dd1abfbf215264a72c38 100644
--- a/web/modules/webform/third_party_settings/webform.honeypot.inc
+++ b/web/modules/webform/third_party_settings/webform.honeypot.inc
@@ -201,7 +201,7 @@ function honeypot_webform_third_party_settings_form_alter(&$form, FormStateInter
  */
 function honeypot_webform_submission_form_alter(&$form, FormStateInterface $form_state, $form_id) {
   // Only add a Honeypot when a webform is initially load.
-  // After a webform is submitted, via a multistep webform and/or saving a draft,
+  // After a webform is submitted, via a multi-step webform and/or saving a draft,
   // we can skip adding a Honeypot.
   if ($form_state->isSubmitted()) {
     return;
diff --git a/web/modules/webform/third_party_settings/webform.maillog.inc b/web/modules/webform/third_party_settings/webform.maillog.inc
index 6bcebaffb0c7e70aa37b646214ef941a4cde798f..54dc33349823c094e62c1380be71cabdec322713 100644
--- a/web/modules/webform/third_party_settings/webform.maillog.inc
+++ b/web/modules/webform/third_party_settings/webform.maillog.inc
@@ -9,6 +9,7 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
 use Drupal\webform\Entity\Webform;
+use Drupal\webform\Plugin\WebformHandler\EmailWebformHandler;
 
 /**
  * Implements hook_webform_submission_form_alter().
@@ -37,7 +38,7 @@ function maillog_webform_submission_form_alter(array &$form, FormStateInterface
   $sends_email = FALSE;
   $handlers = $webform->getHandlers();
   foreach ($handlers as $handler) {
-    if ($handler instanceof \Drupal\webform\Plugin\WebformHandler\EmailWebformHandler) {
+    if ($handler instanceof EmailWebformHandler) {
       $sends_email = TRUE;
     }
   }
@@ -66,9 +67,13 @@ function maillog_webform_submission_form_alter(array &$form, FormStateInterface
   }
 
   // Display warning if no emails are being sent.
-  $build[] = ['#markup' => t('No emails will be sent.'), '#prefix' => ' <b>', '#suffix' => '</b>'];
+  $build[] = [
+    '#markup' => t('No emails will be sent.'),
+    '#prefix' => ' <b>',
+    '#suffix' => '</b>',
+  ];
 
   if ($build) {
-    drupal_set_message(\Drupal::service('renderer')->renderPlain($build), 'warning');
+    \Drupal::messenger()->addWarning(\Drupal::service('renderer')->renderPlain($build));
   }
 }
diff --git a/web/modules/webform/webform.api.php b/web/modules/webform/webform.api.php
index e576472eed6ea1279827404e123dff3160bf764e..303488a11e36e4a6a42816a0a1a490655e30a862 100644
--- a/web/modules/webform/webform.api.php
+++ b/web/modules/webform/webform.api.php
@@ -188,6 +188,62 @@ function hook_webform_third_party_settings_form_alter(array &$form, \Drupal\Core
 
 }
 
+/**
+ * Act on a webform handler when a method is invoked.
+ *
+ * Allows module developers to implement custom logic that can executed before
+ * any webform handler method is invoked.
+ *
+ * This hook can be used to…
+ * - Conditionally enable or disable a handler.
+ * - Alter a handler's configuration.
+ * - Preprocess submission data being passed to a webform handler.
+ *
+ * @param \Drupal\webform\Plugin\WebformHandlerInterface $handler
+ *   A webform handler attached to a webform.
+ * @param string $method_name
+ *   The invoked method name converted to snake case.
+ * @param array $args
+ *   Argument being passed to the handler's method.
+ *
+ * @see \Drupal\webform\Plugin\WebformHandlerInterface
+ */
+function hook_webform_handler_invoke_alter(\Drupal\webform\Plugin\WebformHandlerInterface $handler, $method_name, array &$args) {
+  $webform = $handler->getWebform();
+  $webform_submission = $handler->getWebformSubmission();
+
+  $webform_id = $handler->getWebform()->id();
+  $handler_id = $handler->getHandlerId();
+  $state = $webform_submission->getState();
+}
+
+/**
+ * Act on a webform handler when a specific method is invoked.
+ *
+ * Allows module developers to implement custom logic that can executed before
+ * a specified webform handler method is invoked.
+ *
+ * This hook can be used to…
+ * - Conditionally enable or disable a handler.
+ * - Alter a handler's configuration.
+ * - Preprocess submission data being passed to a webform handler.
+ *
+ * @param \Drupal\webform\Plugin\WebformHandlerInterface $handler
+ *   A webform handler attached to a webform.
+ * @param array $args
+ *   Argument being passed to the handler's method.
+ *
+ * @see \Drupal\webform\Plugin\WebformHandlerInterface
+ */
+function hook_webform_handler_invoke_METHOD_NAME_alter(\Drupal\webform\Plugin\WebformHandlerInterface $handler, array &$args) {
+  $webform = $handler->getWebform();
+  $webform_submission = $handler->getWebformSubmission();
+
+  $webform_id = $handler->getWebform()->id();
+  $handler_id = $handler->getHandlerId();
+  $state = $webform_submission->getState();
+}
+
 /**
  * Return information about external webform libraries.
  *
@@ -287,6 +343,114 @@ function hook_webform_help_info_alter(array &$help) {
   }
 }
 
+/**
+ * Supply additional access rules that should be managed on per-webform level.
+ *
+ * If your module defines any additional access logic that should be managed on
+ * per webform level, this hook is likely to be of use. Provide additional
+ * access rules into the webform access system through this hook. Then website
+ * administrators can assign appropriate grants to your rules for each webform
+ * via admin UI. Whenever you need to check if a user has access to execute a
+ * certain operation you should do the following:
+ *
+ *   \Drupal::entityTypeManager()
+ *     ->getAccessControlHandler('webform_submission')
+ *     ->access($webform_submission, $some_operation, $account);
+ *
+ * This will return either a positive or a negative result depending on what
+ * website administrator has supplied in access settings for the webform in
+ * question.
+ *
+ * @return array
+ *   Array of metadata about additional access rules to be managed on per
+ *   webform basis. Keys should be machine names whereas values are sub arrays
+ *   with the following structure:
+ *   - title: (string) Human friendly title of the rule.
+ *   - description: (array) Renderable array that explains what this access rule
+ *     stands for. Defaults to an empty array.
+ *   - weight: (int) Sorting order of this access rule. Defaults to 0.
+ *   - roles: (string[]) Array of role IDs that should be granted this access
+ *     rule by default. Defaults to an empty array.
+ *   - permissions: (string[]) Array of permissions that should be granted this
+ *     access rule by default. Defaults to an empty array.
+ */
+function hook_webform_access_rules() {
+  return [
+    // A custom operation.
+    'some_operation' => [
+      'title' => t('Some operation'),
+      'weight' => -100,
+      'roles' => ['authenticated'],
+      'permissions' => ['some permission', 'another permission'],
+    ],
+
+    // Custom any and own operations using hook_submission_access().
+    //
+    // - _any: means to grant access to all webform submissions independently
+    //   of authorship
+    // - _own: means to grant access only if the user requesting access is
+    //   the author of the webform submission on which the operation is
+    //   being requested.
+    //
+    // The below 2 operations can be queried together as following:
+    //
+    // \Drupal::entityTypeManager()
+    //   ->getAccessControlHandler('webform_submission')
+    //   ->access($webform_submission, 'some_operation', $account);
+    //
+    // This will return TRUE as long as the $account is has either
+    // 'some_operation_any' or has 'some_operation_own' and is author of
+    // the $webform_submission.
+    //
+    // Note, to implement *_own and *_any you will need to implement
+    // hook_webform_submission_access().
+    //
+    // @see hook_webform_submission_access()
+    'some_operation_any' => [
+      'title' => t('Some operation on ALL webform submissions'),
+      'description' => ['#markup' => t('Allow users to execute such particular operation on all webform submissions independently of whether they are authors of those submissions.')],
+    ],
+    'some_operation_own' => [
+      'title' => t('Some operation on own webform submissions'),
+    ],
+  ];
+}
+
+/**
+ * Alter list of access rules that should be managed on per webform level.
+ *
+ * @param array $access_rules
+ *   Array of known access rules. Its structure is identical to the return of
+ *   hook_webform_access_rules().
+ */
+function hook_webform_access_rules_alter(array &$access_rules) {
+  if (isset($access_rules['some_specific_rule_i_want_to_alter'])) {
+    $access_rules['some_specific_rule_i_want_to_alter']['title'] = t('My very cool altered title!');
+  }
+}
+
+/**
+ * Implements hook_webform_submission_access().
+ *
+ * Implements *_any and *_own operations for a module.
+ */
+function hook_webform_submission_access(\Drupal\webform\WebformSubmissionInterface $webform_submission, $operation, \Drupal\Core\Session\AccountInterface $account) {
+  /** @var \Drupal\webform\WebformAccessRulesManagerInterface $access_rules_manager */
+  $access_rules_manager = \Drupal::service('webform.access_rules_manager');
+
+  // Add support for some module *_any and *_own access rules.
+  $access_rules = \Drupal::moduleHandler()->invoke('MY_MODULE', 'webform_access_rules');
+  $access_any = isset($access_rules[$operation . '_any']) ? $access_rules_manager->checkWebformSubmissionAccess($operation . '_any', $account, $webform_submission) : \Drupal\Core\Access\AccessResult::forbidden();
+  $access_own = (isset($access_rules[$operation . '_own']) && $webform_submission->isOwner($account)) ? $access_rules_manager->checkWebformSubmissionAccess($operation . '_own', $account, $webform_submission) : \Drupal\Core\Access\AccessResult::forbidden();
+  $access = $access_any->orIf($access_own);
+  if ($access->isAllowed()) {
+    return $access;
+  }
+  else {
+    return \Drupal\Core\Access\AccessResult::neutral();
+  }
+}
+
 /**
  * Act on a custom message being displayed, closed or reset.
  *
@@ -294,11 +458,10 @@ function hook_webform_help_info_alter(array &$help) {
  *   closed: Returns TRUE if the message is closed.
  *   close: Sets the message's state to closed.
  *   reset: Resets the message's closed state.
- *
  * @param string $id
  *   The message id.
  *
- * @return mixed|boolean
+ * @return mixed|bool
  *   TRUE if message is closed, else NULL
  *
  * @internal
diff --git a/web/modules/webform/webform.info.yml b/web/modules/webform/webform.info.yml
index 4e23b2d70ed09f568c1104ed5e5369d28593562a..10b8a6ebb97b20ffa1000040ec7acd9d2f527250 100644
--- a/web/modules/webform/webform.info.yml
+++ b/web/modules/webform/webform.info.yml
@@ -6,13 +6,20 @@ package: Webform
 configure: entity.webform.collection
 dependencies:
   - 'drupal:field'
-  - 'drupal:system (>= 8.3)'
+  - 'drupal:system (>= 8.5)'
   - 'drupal:user'
 test_dependencies:
+  - 'address:address'
+  - 'captcha:captcha'
+  - 'chosen:chosen'
+  - 'jsonapi:jsonapi'
+  - 'mailsystem:mailsystem'
+  - 'select2:select2'
+  - 'telephone_validation/telephone_validation'
   - 'token:token'
 
-# Information added by Drupal.org packaging script on 2018-04-26
-version: '8.x-5.0-rc12'
+# Information added by Drupal.org packaging script on 2019-03-28
+version: '8.x-5.2'
 core: '8.x'
 project: 'webform'
-datestamp: 1524706694
+datestamp: 1553768595
diff --git a/web/modules/webform/webform.install b/web/modules/webform/webform.install
index d84ed430de3aa8e4f668b49309ead4ee222a5a83..c1e2b2acedae7bd75874ade61dc9b889213bc07d 100644
--- a/web/modules/webform/webform.install
+++ b/web/modules/webform/webform.install
@@ -6,13 +6,13 @@
  */
 
 // Webform install helper functions.
-include_once 'includes/webform.install.inc';
+include_once __DIR__ . '/includes/webform.install.inc';
 
 // Webform requirements.
-include_once 'includes/webform.install.requirements.inc';
+include_once __DIR__ . '/includes/webform.install.requirements.inc';
 
 // Webform update hooks.
-include_once 'includes/webform.install.update.inc';
+include_once __DIR__ . '/includes/webform.install.update.inc';
 
 /**
  * Implements hook_uninstall().
@@ -26,6 +26,10 @@ function webform_uninstall() {
   $mail_plugins = $config->get('interface');
   unset($mail_plugins['webform']);
   $config->set('interface', $mail_plugins)->save();
+
+  // Delete editor uploaded files.
+  $config = \Drupal::configFactory()->get('webform.settings');
+  _webform_config_delete($config);
 }
 
 /**
diff --git a/web/modules/webform/webform.libraries.yml b/web/modules/webform/webform.libraries.yml
index 36b37dbfcffa28267407b0ec741479bd42f6aaa6..f144abf65c20f308baab0716d59975bf9fe05285 100644
--- a/web/modules/webform/webform.libraries.yml
+++ b/web/modules/webform/webform.libraries.yml
@@ -6,11 +6,27 @@ webform.admin:
     theme:
       css/webform.admin.css: {}
   js:
-    js/webform.admin.js:  {}
+    js/webform.admin.js: {}
   dependencies:
+    - core/jquery
+    - core/drupal
     - webform/webform.form
+    - webform/webform.filter
     - webform/webform.admin.dropbutton
 
+webform.filter:
+  version: VERSION
+  css:
+    theme:
+      css/webform.filter.css: {}
+  js:
+    js/webform.filter.js: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/drupal.announce
+    - core/drupal.debounce
+
 webform.admin.composite:
   version: VERSION
   css:
@@ -25,11 +41,10 @@ webform.admin.dialog:
     theme:
       css/webform.admin.css: {}
   js:
-    js/webform.admin.js:  {}
-    js/webform.dialog.js:  {}
+    js/webform.admin.js: {}
   dependencies:
     - core/drupal.dialog.ajax
-    - core/jquery.ui.dialog
+    - webform/jquery.ui.dialog
     - webform/webform.ajax
     - webform/webform.form
     - webform/webform.form.tabs
@@ -46,10 +61,26 @@ webform.admin.dropbutton:
     theme:
       css/webform.admin.dropbutton.css: {}
   js:
-    js/webform.admin.dropbutton.js:  {}
+    js/webform.admin.dropbutton.js: {}
   dependencies:
     - core/drupal.dropbutton
 
+webform.admin.settings:
+  version: VERSION
+  css:
+    theme:
+      css/webform.admin.settings.css: {}
+
+webform.admin.tabledrag:
+  version: VERSION
+  css:
+    theme:
+      css/webform.admin.tabledrag.css: {}
+  js:
+    js/webform.admin.tabledrag.js: {}
+  dependencies:
+    - core/drupal.tabledrag
+
 webform.admin.translation:
   version: VERSION
   css:
@@ -77,20 +108,33 @@ webform.ajax:
     theme:
       css/webform.ajax.css: {}
   js:
-    js/webform.ajax.js:  {}
+    js/webform.ajax.js: {}
   dependencies:
     - core/drupal
     - core/drupalSettings
     - core/drupal.ajax
+    - core/drupal.announce
     - core/jquery.once
     - core/jquery.form
 
-# Block
+# Announce.
+
+webform.announce:
+  version: VERSION
+  js:
+    js/webform.announce.js: {}
+  dependencies:
+    - core/jquery
+    - core/jquery.once
+    - core/drupal
+    - core/drupal.announce
+
+# Block.
 
 webform.block:
   version: VERSION
   js:
-    js/webform.block.js:  {}
+    js/webform.block.js: {}
   dependencies:
     - webform/webform.element.codemirror.yaml
     - webform/webform.element.select2
@@ -112,12 +156,13 @@ webform.confirmation:
 webform.confirmation.modal:
   version: VERSION
   js:
-    js/webform.confirmation.modal.js:  {}
+    js/webform.confirmation.modal.js: {}
   dependencies:
     - core/drupal
     - core/drupal.dialog
     - core/drupal.dialog.position
     - core/jquery.once
+    - core/jquery.ui.dialog
     - webform/webform.confirmation
 
 # Contribute.
@@ -133,10 +178,20 @@ webform.contribute:
 webform.contextual:
   version: VERSION
   js:
-    js/webform.contextual.js:  {}
+    js/webform.contextual.js: {}
   dependencies:
     - core/jquery.once
 
+# Dialog.
+
+webform.dialog:
+  version: VERSION
+  js:
+    js/webform.dialog.js: {}
+  dependencies:
+    - core/drupal.dialog.ajax
+    - webform/jquery.ui.dialog
+
 # Help.
 
 webform.help:
@@ -145,7 +200,7 @@ webform.help:
     theme:
       css/webform.help.css: {}
   js:
-    js/webform.help.js:  {}
+    js/webform.help.js: {}
 
 # More.
 
@@ -190,6 +245,16 @@ webform.progress.tracker:
   dependencies:
     - webform/libraries.progress-tracker
 
+# Off canvas.
+
+webform.off_canvas:
+  version: VERSION
+  js:
+    js/webform.off-canvas.js: {}
+  dependencies:
+    - core/drupal.debounce
+    - core/drupal.dialog.off_canvas
+
 # Promotions.
 
 webform.promotions:
@@ -203,10 +268,20 @@ webform.promotions:
 webform.states:
   version: VERSION
   js:
-    js/webform.states.js:  {}
+    js/webform.states.js: {}
   dependencies:
     - core/drupal.states
 
+# Token.
+
+webform.token:
+  version: VERSION
+  css:
+    component:
+      css/webform.token.css: {}
+  dependencies:
+    - webform/webform.element.more
+
 # Tooltip.
 
 webform.tooltip:
@@ -240,9 +315,10 @@ webform.form:
     component:
       css/webform.form.css: {}
   js:
-    js/webform.form.js:  {}
+    js/webform.form.js: {}
   dependencies:
     - core/drupal
+    - core/drupal.form
     - core/jquery.once
     - webform/webform.states
 
@@ -250,29 +326,30 @@ webform.form.disable_back:
   version: VERSION
   header: true
   js:
-    js/webform.form.disable_back.js:  {preprocess: false}
+    js/webform.form.disable_back.js: { preprocess: false }
 
 webform.form.submit_back:
   version: VERSION
   header: true
   js:
-    js/webform.form.submit_back.js:  {preprocess: false}
+    js/webform.form.submit_back.js: { preprocess: false }
   dependencies:
     - core/jquery
 
 webform.form.submit_once:
   version: VERSION
   js:
-    js/webform.form.submit_once.js:  {}
+    js/webform.form.submit_once.js: {}
   dependencies:
     - core/drupal
+    - core/drupal.ajax
     - core/jquery.once
     - system/base
 
 webform.form.tabs:
   version: VERSION
   js:
-    js/webform.form.tabs.js:  {}
+    js/webform.form.tabs.js: {}
   dependencies:
     - core/drupal
     - core/jquery.once
@@ -281,31 +358,33 @@ webform.form.tabs:
 webform.form.unsaved:
   version: VERSION
   js:
-    js/webform.form.unsaved.js:  {}
+    js/webform.form.unsaved.js: {}
   dependencies:
     - core/jquery.once
     - core/drupal
 
-webform.form.wizard:
+webform.wizard.pages:
   version: VERSION
+  css:
+    component:
+      css/webform.wizard.pages.css: {}
   js:
-    js/webform.form.wizard.js:  {}
+    js/webform.wizard.pages.js: {}
   dependencies:
     - core/jquery.once
     - core/drupal
 
-# Element.
-
-# Drupal 8.3.x and below
-webform.element.buttons.buttonset:
+webform.wizard.track:
   version: VERSION
   js:
-    js/webform.element.buttons.buttonset.js: {}
+    js/webform.wizard.track.js: {}
   dependencies:
-    - core/jquery.ui.button
+    - core/jquery.once
+    - core/drupal
 
-# Drupal 8.4.x and above
-webform.element.buttons.checkboxradio:
+# Element.
+
+webform.element.buttons:
   version: VERSION
   js:
     # Issue #2906737: With the new jquery 3 checkboxradio and selectmenu error.
@@ -313,7 +392,7 @@ webform.element.buttons.checkboxradio:
     /core/assets/vendor/jquery.ui/ui/form-reset-mixin-min.js: { minified: true }
     /core/assets/vendor/jquery.ui/ui/escape-selector-min.js: { minified: true }
     /core/assets/vendor/jquery.ui/ui/widgets/checkboxradio-min.js: { minified: true }
-    js/webform.element.buttons.checkboxradio.js: {}
+    js/webform.element.buttons.js: {}
   # Issue #2933980: JQuery UI CSS dependencies are not met in core.
   # @see https://www.drupal.org/node/2933980
   css:
@@ -343,7 +422,7 @@ webform.element.codemirror.text:
     component:
       css/webform.element.codemirror.css: {}
   js:
-    js/webform.element.codemirror.js: {weight: -1}
+    js/webform.element.codemirror.js: { weight: -1 }
   dependencies:
     - webform/libraries.codemirror.text
 
@@ -353,7 +432,7 @@ webform.element.codemirror.yaml:
     component:
       css/webform.element.codemirror.css: {}
   js:
-    js/webform.element.codemirror.js: {weight: -1}
+    js/webform.element.codemirror.js: { weight: -1 }
   dependencies:
     - webform/libraries.codemirror.yaml
 
@@ -363,7 +442,7 @@ webform.element.codemirror.html:
     component:
       css/webform.element.codemirror.css: {}
   js:
-    js/webform.element.codemirror.js: {weight: -1}
+    js/webform.element.codemirror.js: { weight: -1 }
   dependencies:
     - webform/libraries.codemirror.html
 
@@ -373,7 +452,7 @@ webform.element.codemirror.htmlmixed:
     component:
       css/webform.element.codemirror.css: {}
   js:
-    js/webform.element.codemirror.js: {weight: -1}
+    js/webform.element.codemirror.js: { weight: -1 }
   dependencies:
     - webform/libraries.codemirror.htmlmixed
 
@@ -383,7 +462,7 @@ webform.element.codemirror.css:
     component:
       css/webform.element.codemirror.css: {}
   js:
-    js/webform.element.codemirror.js: {weight: -1}
+    js/webform.element.codemirror.js: { weight: -1 }
   dependencies:
     - webform/libraries.codemirror.css
 
@@ -393,7 +472,7 @@ webform.element.codemirror.javascript:
     component:
       css/webform.element.codemirror.css: {}
   js:
-    js/webform.element.codemirror.js: {weight: -1}
+    js/webform.element.codemirror.js: { weight: -1 }
   dependencies:
     - webform/libraries.codemirror.javascript
 
@@ -403,7 +482,7 @@ webform.element.codemirror.php:
     component:
       css/webform.element.codemirror.css: {}
   js:
-    js/webform.element.codemirror.js: {weight: -1}
+    js/webform.element.codemirror.js: { weight: -1 }
   dependencies:
     - webform/libraries.codemirror.php
 
@@ -413,7 +492,7 @@ webform.element.codemirror.twig:
     component:
       css/webform.element.codemirror.css: {}
   js:
-    js/webform.element.codemirror.js: {weight: -1}
+    js/webform.element.codemirror.js: { weight: -1 }
   dependencies:
     - webform/libraries.codemirror.twig
 
@@ -439,23 +518,47 @@ webform.element.composite:
     - core/drupal
     - core/jquery.once
 
+webform.element.computed:
+  version: VERSION
+  css:
+    component:
+      css/webform.element.computed.css: {}
+  js:
+    js/webform.element.computed.js: {}
+  dependencies:
+    - core/drupal.debounce
+    - core/jquery.once
+    - webform/webform.announce
+
 webform.element.counter:
   version: VERSION
+  css:
+    component:
+      css/webform.element.counter.css: {}
   js:
     js/webform.element.counter.js: {}
   dependencies:
     - core/drupal
     - core/jquery.once
-    - webform/libraries.jquery.word-and-character-counter
+    - webform/libraries.jquery.textcounter
 
 webform.element.date:
   version: VERSION
+  css:
+    component:
+      css/webform.element.date.css: {}
   js:
     js/webform.element.date.js: {}
   dependencies:
     - core/drupal.date
     - core/drupalSettings
 
+webform.element.datelist:
+  version: VERSION
+  css:
+    component:
+      css/webform.element.datelist.css: {}
+
 webform.element.details.save:
   version: VERSION
   js:
@@ -473,6 +576,7 @@ webform.element.details.toggle:
     js/webform.element.details.toggle.js: {}
   dependencies:
     - core/drupal
+    - core/drupal.announce
     - core/jquery.once
 
 webform.element.file.button:
@@ -556,24 +660,62 @@ webform.element.inputmask:
     - core/jquery.once
     - webform/libraries.jquery.inputmask
 
+webform.element.inputhide:
+  version: VERSION
+  js:
+    js/webform.element.inputhide.js: {}
+  dependencies:
+    - core/drupal
+    - core/jquery.once
+
 webform.element.likert:
   version: VERSION
   css:
     component:
       css/webform.element.likert.css: {}
 
-webform.element.location:
+webform.element.location.geocomplete:
   version: VERSION
   css:
     component:
-      css/webform.element.location.css: {}
+      css/webform.element.location.geocomplete.css: {}
   js:
-    js/webform.element.location.js: {}
+    js/webform.element.location.geocomplete.js: {}
   dependencies:
     - core/drupal
     - core/jquery.once
     - webform/libraries.jquery.geocomplete
 
+webform.element.location.places:
+  version: VERSION
+  css:
+    component:
+      css/webform.element.location.places.css: {}
+  js:
+    js/webform.element.location.places.js: {}
+  dependencies:
+    - core/drupal
+    - core/jquery.once
+    - webform/libraries.algolia.places
+
+webform.element.managed_file:
+  version: VERSION
+  css:
+    component:
+      css/webform.element.managed_file.css: {}
+  js:
+    js/webform.element.managed_file.js: {}
+  dependencies:
+    - core/drupal
+    - core/jquery.once
+    - file/drupal.file
+
+webform.element.mapping:
+  version: VERSION
+  css:
+    component:
+      css/webform.element.mapping.css: {}
+
 webform.element.message:
   version: VERSION
   css:
@@ -685,6 +827,12 @@ webform.element.states:
   css:
     component:
       css/webform.element.states.css: {}
+  js:
+    js/webform.element.states.js: {}
+  dependencies:
+    - core/drupal
+    - core/jquery.once
+    - core/jquery.ui.autocomplete
 
 webform.element.telephone:
   version: VERSION
@@ -724,6 +872,8 @@ webform.element.text_format:
 
 webform.element.tableselect:
   version: VERSION
+  js:
+    js/webform.element.tableselect.js: {}
   css:
     component:
       css/webform.element.tableselect.css: {}
@@ -776,32 +926,42 @@ webform.composite.telephone:
   dependencies:
     - webform/webform.composite
 
+# jQuery UI.
+
+jquery.ui.dialog:
+  version: VERSION
+  js:
+    js/webform.jquery.ui.dialog.js: {}
+  dependencies:
+    - core/jquery.ui.dialog
+
 # Contrib Module.
 
 imce.input:
   version: VERSION
   js:
-    js/webform.imce.js:  {}
+    js/webform.imce.js: {}
   dependencies:
     - imce/drupal.imce.input
 
+
 # External libraries.
 
 libraries.codemirror.text:
   remote: https://github.com/codemirror/codemirror
-  version: &webform_codemirror_version '5.36.0'
+  version: &webform_codemirror_version '5.44.0'
   license: &webform_codemirror_license
     name: MIT
     url: http://codemirror.net/LICENSE
     gpl-compatible: true
   cdn: &webform_codemirror_cdn
-    /libraries/codemirror/lib/: https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.36.0/
-    /libraries/codemirror/: https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.36.0/
+    /libraries/codemirror/lib/: https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.44.0/
+    /libraries/codemirror/: https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.44.0/
   css:
     component:
       /libraries/codemirror/lib/codemirror.css: {}
   js:
-    /libraries/codemirror/lib/codemirror.js: {weight: -1}
+    /libraries/codemirror/lib/codemirror.js: { weight: -1 }
     /libraries/codemirror/addon/runmode/runmode.js: {}
     /libraries/codemirror/addon/display/placeholder.js: {}
 
@@ -834,7 +994,7 @@ libraries.codemirror.htmlmixed:
   dependencies:
     - webform/libraries.codemirror.text
     - webform/libraries.codemirror.css
-    - webform/libraries.codemirror.js
+    - webform/libraries.codemirror.javascript
 
 libraries.codemirror.css:
   version: *webform_codemirror_version
@@ -861,7 +1021,6 @@ libraries.codemirror.php:
   js:
     /libraries/codemirror/mode/php/php.js: {}
     /libraries/codemirror/mode/clike/clike.js: {}
-
   dependencies:
     - webform/libraries.codemirror.text
 
@@ -874,19 +1033,17 @@ libraries.codemirror.twig:
   dependencies:
     - webform/libraries.codemirror.text
 
-libraries.jquery.word-and-character-counter:
-  remote: https://github.com/qwertypants/jQuery-Word-and-Character-Counter-Plugin
-  version: '2.5.1'
+libraries.algolia.places:
+  remote: https://github.com/algolia/places
+  version: '1.16.1'
   license:
     name: MIT
-    url: http://opensource.org/licenses/mit-license.php
+    url: https://github.com/algolia/places/blob/master/LICENSE
     gpl-compatible: true
   cdn:
-    /libraries/jquery.word-and-character-counter/: https://cdn.rawgit.com/qwertypants/jQuery-Word-and-Character-Counter-Plugin/2.5.1/
+    /libraries/algolia.places/dist/cdn/: https://cdn.jsdelivr.net/npm/places.js@1.16.1/dist/cdn/
   js:
-    /libraries/jquery.word-and-character-counter/jquery.word-and-character-counter.min.js: {}
-  dependencies:
-    - core/jquery
+    /libraries/algolia.places/dist/cdn/places.js: {}
 
 libraries.jquery.geocomplete:
   remote: http://ubilabs.github.io/geocomplete/
@@ -899,7 +1056,7 @@ libraries.jquery.geocomplete:
     /libraries/geocomplete/: https://cdnjs.cloudflare.com/ajax/libs/geocomplete/1.7.0/
   js:
     https://maps.googleapis.com/maps/api/js?key=API_KEY&libraries=places: {}
-    /libraries/geocomplete/jquery.geocomplete.min.js: {}
+    /libraries/geocomplete/jquery.geocomplete.min.js: { minified: true }
   dependencies:
     - core/jquery
 
@@ -959,33 +1116,34 @@ libraries.jquery.icheck.square:
 
 libraries.jquery.inputmask:
   remote: https://github.com/RobinHerbots/jquery.inputmask
-  version: '3.3.11'
+  version: '4.0.6'
   license:
     name: MIT
     url: http://opensource.org/licenses/mit-license.php
     gpl-compatible: true
   cdn:
-    /libraries/jquery.inputmask/: https://cdn.rawgit.com/RobinHerbots/Inputmask/3.3.11/
+    /libraries/jquery.inputmask/: https://cdn.jsdelivr.net/gh/RobinHerbots/Inputmask@4.0.6/
   js:
-    /libraries/jquery.inputmask/dist/min/jquery.inputmask.bundle.min.js: {}
+    /libraries/jquery.inputmask/dist/min/jquery.inputmask.bundle.min.js: { minified: true }
   dependencies:
     - core/jquery
 
 libraries.jquery.intl-tel-input:
   remote: https://github.com/jackocnr/intl-tel-input
-  version: 'v12.1.12'
+  version: 'v15.0.1'
   license:
     name: MIT
     url: https://github.com/jackocnr/intl-tel-input/blob/master/LICENSE
     gpl-compatible: true
   cdn:
-    /libraries/jquery.intl-tel-input/build/: https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/12.1.12/
+    /libraries/jquery.intl-tel-input/: https://cdn.jsdelivr.net/gh/jackocnr/intl-tel-input@v15.0.1/
   css:
     component:
-      /libraries/jquery.intl-tel-input/build/css/intlTelInput.css: {}
+      /libraries/jquery.intl-tel-input/build/css/intlTelInput.min.css: { minified: true }
   js:
-    /libraries/jquery.intl-tel-input/build/js/utils.js : {}
-    /libraries/jquery.intl-tel-input/build/js/intlTelInput.min.js : {}
+    /libraries/jquery.intl-tel-input/build/js/intlTelInput.min.js: { minified: true }
+    /libraries/jquery.intl-tel-input/build/js/intlTelInput-jquery.min.js: { minified: true }
+
   dependencies:
     - core/jquery
 
@@ -997,12 +1155,12 @@ libraries.jquery.rateit:
     url: https://github.com/gjunge/rateit.js/blob/master/LICENSE.md
     gpl-compatible: true
   cdn:
-    /libraries/jquery.rateit/: https://cdn.rawgit.com/gjunge/rateit.js/1.1.1/
+    /libraries/jquery.rateit/: https://cdn.jsdelivr.net/gh/gjunge/rateit.js@1.1.1/
   css:
     component:
       /libraries/jquery.rateit/scripts/rateit.css: {}
   js:
-    /libraries/jquery.rateit/scripts/jquery.rateit.min.js: {}
+    /libraries/jquery.rateit/scripts/jquery.rateit.min.js: { minified: true }
   dependencies:
     - core/jquery
 
@@ -1017,43 +1175,57 @@ libraries.jquery.select2:
     /libraries/jquery.select2/dist/: https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/
   css:
     component:
-      /libraries/jquery.select2/dist/css/select2.min.css: {}
+      /libraries/jquery.select2/dist/css/select2.min.css: { minified: true }
   js:
-    /libraries/jquery.select2/dist/js/select2.min.js: {}
+    /libraries/jquery.select2/dist/js/select2.min.js: { minified: true }
   dependencies:
     - core/jquery
 
 libraries.jquery.chosen:
   remote: https://harvesthq.github.io/chosen/
-  version: 'v1.8.3'
+  version: 'v1.8.7'
   license:
     name: MIT
     url: https://github.com/harvesthq/chosen/blob/master/LICENSE.md
     gpl-compatible: true
   cdn:
-    /libraries/jquery.chosen/: https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.3/
+    /libraries/jquery.chosen/: https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.7/
   css:
     component:
-      /libraries/jquery.chosen/chosen.css: {}
+      /libraries/jquery.chosen/chosen.min.css: { minified: true }
+  js:
+    /libraries/jquery.chosen/chosen.jquery.min.js: { minified: true }
+  dependencies:
+    - core/jquery
+
+libraries.jquery.textcounter:
+  remote: https://github.com/ractoon/jQuery-Text-Counter
+  version: '0.8.0'
+  license:
+    name: MIT
+    url: http://opensource.org/licenses/mit-license.php
+    gpl-compatible: true
+  cdn:
+    /libraries/jquery.textcounter/: https://cdn.jsdelivr.net/gh/ractoon/jQuery-Text-Counter@0.8.0/
   js:
-    /libraries/jquery.chosen/chosen.jquery.js: {}
+    /libraries/jquery.textcounter/textcounter.min.js: { minified: true }
   dependencies:
     - core/jquery
 
 libraries.jquery.timepicker:
   remote: https://github.com/jonthornton/jquery-timepicker
-  version: '1.11.13'
+  version: '1.11.14'
   license:
     name: MIT
     url: http://opensource.org/licenses/mit-license.php
     gpl-compatible: true
   cdn:
-    /libraries/jquery.timepicker/: https://cdn.rawgit.com/jonthornton/jquery-timepicker/1.11.13/
+    /libraries/jquery.timepicker/: https://cdn.jsdelivr.net/gh/jonthornton/jquery-timepicker@1.11.14/
   css:
     component:
-      /libraries/jquery.timepicker/jquery.timepicker.css: {}
+      /libraries/jquery.timepicker/jquery.timepicker.min.css: { minified: true }
   js:
-    /libraries/jquery.timepicker/jquery.timepicker.min.js: {}
+    /libraries/jquery.timepicker/jquery.timepicker.min.js: { minified: true }
   dependencies:
     - core/jquery
 
@@ -1065,12 +1237,12 @@ libraries.jquery.toggles:
     url: http://opensource.org/licenses/mit-license.php
     gpl-compatible: true
   cdn:
-    /libraries/jquery.toggles/: https://cdn.rawgit.com/simontabor/jquery-toggles/v4.0.0/
+    /libraries/jquery.toggles/: https://cdn.jsdelivr.net/gh/simontabor/jquery-toggles@v4.0.0/
   css:
     component:
       /libraries/jquery.toggles/css/toggles-full.css: {}
   js:
-    /libraries/jquery.toggles/toggles.min.js: {}
+    /libraries/jquery.toggles/toggles.min.js: { minified: true }
   dependencies:
     - core/jquery
 
@@ -1082,7 +1254,7 @@ libraries.progress-tracker:
     url: https://github.com/NigelOToole/progress-tracker/blob/master/LICENSE
     gpl-compatible: true
   cdn:
-    /libraries/progress-tracker/: https://cdn.rawgit.com/NigelOToole/progress-tracker/v1.4.0/
+    /libraries/progress-tracker/: https://cdn.jsdelivr.net/gh/NigelOToole/progress-tracker@v1.4.0/
   css:
     component:
       /libraries/progress-tracker/app/styles/progress-tracker.css: {}
@@ -1095,6 +1267,6 @@ libraries.signature_pad:
     url: http://opensource.org/licenses/mit-license.php
     gpl-compatible: true
   cdn:
-    /libraries/signature_pad/: https://cdn.rawgit.com/szimek/signature_pad/v2.3.0/
+    /libraries/signature_pad/: https://cdn.jsdelivr.net/gh/szimek/signature_pad@v2.3.0/
   js:
     /libraries/signature_pad/example/js/signature_pad.js: {}
diff --git a/web/modules/webform/webform.links.action.yml b/web/modules/webform/webform.links.action.yml
index 55b34502d70e148e2c2820e072c3be8d8875ad69..db4554a06be8b0953c15193b89e0974a46ac43b7 100644
--- a/web/modules/webform/webform.links.action.yml
+++ b/web/modules/webform/webform.links.action.yml
@@ -1,5 +1,29 @@
+entity.webform.add_form:
+  route_name: entity.webform.add_form
+  title: 'Add webform'
+  class: '\Drupal\webform\Plugin\Menu\LocalAction\WebformDialogLocalAction'
+  dialog: narrow
+  appears_on:
+    - entity.webform.collection
+
 entity.webform_options.add_form:
   route_name: entity.webform_options.add_form
   title: 'Add options'
   appears_on:
     - entity.webform_options.collection
+
+entity.webform.handler.add_email:
+  route_name: entity.webform.handler.add_email
+  title: 'Add email'
+  off_canvas: normal
+  class: '\Drupal\webform\Plugin\Menu\LocalAction\WebformDialogLocalAction'
+  appears_on:
+    - entity.webform.handlers
+
+entity.webform.handler:
+  route_name: entity.webform.handler
+  title: 'Add handler'
+  dialog: normal
+  class: '\Drupal\webform\Plugin\Menu\LocalAction\WebformDialogLocalAction'
+  appears_on:
+    - entity.webform.handlers
diff --git a/web/modules/webform/webform.links.menu.yml b/web/modules/webform/webform.links.menu.yml
index 0146635dbe35382ef0e609323eb256b1dd8e020c..f31ef94d4a7dfefcb20b93c0f567a9afa5fbefdb 100644
--- a/web/modules/webform/webform.links.menu.yml
+++ b/web/modules/webform/webform.links.menu.yml
@@ -3,3 +3,8 @@ entity.webform.collection:
   description: 'Create webforms and manage submissions.'
   parent: system.admin_structure
   route_name: entity.webform.collection
+webform.reports_plugins:
+  title: 'Webform plugins'
+  parent: system.admin_reports
+  description: 'Overview of plugins used by the Webform module.'
+  route_name: webform.reports_plugins.elements
diff --git a/web/modules/webform/webform.links.task.yml b/web/modules/webform/webform.links.task.yml
index 9739b7f93cebd6532ae8db5ab8536251bced61e0..1f1c5dc8ce729029b4011743e04aa63c4925223e 100644
--- a/web/modules/webform/webform.links.task.yml
+++ b/web/modules/webform/webform.links.task.yml
@@ -26,12 +26,6 @@ entity.webform_submission.collection_purge:
   parent_id: entity.webform_submission.collection
   weight: 10
 
-entity.webform_submission.collection_log:
-  title: 'Log'
-  route_name: entity.webform_submission.collection_log
-  parent_id: entity.webform_submission.collection
-  weight: 20
-
 # Settings.
 
 webform.config:
@@ -86,33 +80,7 @@ webform.config.advanced:
   title: 'Advanced'
   route_name: webform.config.advanced
   parent_id: webform.config
-  weight: 80
-
-# Plugins.
-
-webform.plugins:
-  title: 'Plugins'
-  route_name: webform.element_plugins
-  base_route: entity.webform.collection
-  weight: 40
-
-webform.plugins.elements:
-  title: 'Elements'
-  route_name: webform.element_plugins
-  parent_id: webform.plugins
-  weight: 0
-
-webform.plugins.handlers:
-  title: 'Handlers'
-  route_name: webform.handler_plugins
-  parent_id: webform.plugins
-  weight: 10
-
-webform.plugins.exporters:
-  title: 'Exporters'
-  route_name: webform.exporter_plugins
-  parent_id: webform.plugins
-  weight: 20
+  weight: 100
 
 webform.addons:
   title: 'Add-ons'
@@ -120,11 +88,23 @@ webform.addons:
   base_route: entity.webform.collection
   weight: 50
 
+webform.help:
+  title: 'Help'
+  route_name: webform.help
+  base_route: entity.webform.collection
+  weight: 60
+
+webform.help.videos:
+  title: 'Videos'
+  route_name: webform.help
+  parent_id: webform.help
+  weight: 0
+
 webform.contribute:
   title: 'Contribute'
   route_name: webform.contribute
-  base_route: entity.webform.collection
-  weight: 50
+  parent_id: webform.help
+  weight: 10
 
 # Form
 
@@ -168,12 +148,6 @@ entity.webform.results_clear:
   parent_id: entity.webform.results
   weight: 20
 
-entity.webform.results_log:
-  title: 'Log'
-  route_name: entity.webform.results_log
-  parent_id: entity.webform.results
-  weight: 30
-
 # Webform edit (build).
 
 entity.webform.edit_form:
@@ -232,6 +206,14 @@ entity.webform.settings_access:
   parent_id: entity.webform.settings
   weight: 60
 
+# Export
+
+entity.webform.export_form:
+  title: 'Export'
+  route_name: entity.webform.export_form
+  base_route: entity.webform.canonical
+  weight: 60
+
 # Submission
 
 entity.webform_submission.canonical:
@@ -294,18 +276,6 @@ entity.webform_submission.resend_form:
   base_route: entity.webform_submission.canonical
   weight: 30
 
-entity.webform_submission.delete_form:
-  title: 'Delete'
-  route_name: entity.webform_submission.delete_form
-  base_route: entity.webform_submission.canonical
-  weight: 40
-
-entity.webform_submission.log:
-  title: 'Log'
-  route_name: entity.webform_submission.log
-  base_route: entity.webform_submission.canonical
-  weight: 50
-
 # User Submission
 
 entity.webform.user.submission:
@@ -320,8 +290,35 @@ entity.webform.user.submission.edit:
   base_route: entity.webform.user.submission
   weight: 10
 
-entity.webform.user.submission.delete:
-  title: 'Delete'
-  route_name: entity.webform.user.submission.delete
+entity.webform.user.submission.duplicate:
+  title: 'Duplicate'
+  route_name: entity.webform.user.submission.duplicate
   base_route: entity.webform.user.submission
   weight: 20
+
+# Webform Plugins.
+
+webform.reports_plugins.elements:
+  title: 'Elements'
+  route_name: webform.reports_plugins.elements
+  base_route: webform.reports_plugins.elements
+  weight: 0
+
+webform.reports_plugins.handlers:
+  title: 'Handlers'
+  route_name: webform.reports_plugins.handlers
+  base_route: webform.reports_plugins.elements
+  weight: 10
+
+webform.reports_plugins.exporters:
+  title: 'Exporters'
+  route_name: webform.reports_plugins.exporters
+  base_route: webform.reports_plugins.elements
+  weight: 20
+
+# User Submissions.
+
+entity.webform_submission.user:
+  title: 'Submissions'
+  route_name: entity.webform_submission.user
+  base_route: entity.user.canonical
diff --git a/web/modules/webform/webform.module b/web/modules/webform/webform.module
index 28e42a6f224d7081d57f1e57cb50096571a05d3e..8f4db07dd165b81ca64a9b3e9af3a32448330894 100644
--- a/web/modules/webform/webform.module
+++ b/web/modules/webform/webform.module
@@ -10,8 +10,10 @@
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Asset\AttachedAssetsInterface;
 use Drupal\Core\Breadcrumb\Breadcrumb;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Database\Query\AlterableInterface;
 use Drupal\Core\Render\Element;
+use Drupal\Core\Render\Markup;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -22,7 +24,9 @@
 use Drupal\webform\Entity\WebformSubmission;
 use Drupal\webform\Element\WebformMessage;
 use Drupal\webform\Plugin\WebformElement\ManagedFile;
+use Drupal\webform\Utility\Mail;
 use Drupal\webform\Utility\WebformArrayHelper;
+use Drupal\webform\Utility\WebformElementHelper;
 use Drupal\webform\Utility\WebformDialogHelper;
 use Drupal\webform\Utility\WebformOptionsHelper;
 use Drupal\webform\WebformInterface;
@@ -33,6 +37,7 @@
 require_once __DIR__ . '/includes/webform.options.inc';
 require_once __DIR__ . '/includes/webform.theme.inc';
 require_once __DIR__ . '/includes/webform.translation.inc';
+require_once __DIR__ . '/includes/webform.editor.inc';
 
 /**
  * Implements hook_help().
@@ -102,11 +107,18 @@ function webform_modules_uninstalled($modules) {
   // Remove uninstalled module's third party settings from admin settings.
   $config = \Drupal::configFactory()->getEditable('webform.settings');
   $third_party_settings = $config->get('third_party_settings');
+
+  $has_third_party_settings = FALSE;
   foreach ($modules as $module) {
-    unset($third_party_settings[$module]);
+    if (isset($third_party_settings[$module])) {
+      $has_third_party_settings = TRUE;
+      unset($third_party_settings[$module]);
+    }
+  }
+  if ($has_third_party_settings) {
+    $config->set('third_party_settings', $third_party_settings);
+    $config->save();
   }
-  $config->set('third_party_settings', $third_party_settings);
-  $config->save();
 
   // Check HTML email provider support as modules are ininstalled.
   /** @var \Drupal\webform\WebformEmailProviderInterface $email_provider */
@@ -139,6 +151,10 @@ function webform_local_tasks_alter(&$local_tasks) {
     $local_tasks['config_translation.local_tasks:entity.webform.config_translation_overview']['base_route'] = 'entity.webform.canonical';
   }
   if (isset($local_tasks['config_translation.local_tasks:config_translation.item.overview.webform.config'])) {
+    // Set weight to 110 so that the 'Translate' tab comes after
+    // the 'Advanced' tab.
+    // @see webform.links.task.yml
+    $local_tasks['config_translation.local_tasks:config_translation.item.overview.webform.config']['weight'] = 110;
     $local_tasks['config_translation.local_tasks:config_translation.item.overview.webform.config']['parent_id'] = 'webform.config';
   }
 
@@ -153,10 +169,18 @@ function webform_local_tasks_alter(&$local_tasks) {
  * Implements hook_menu_local_tasks_alter().
  */
 function webform_menu_local_tasks_alter(&$data, $route_name) {
-  // Change 'Translate *' tab to be just label 'Translate'.
-  if (isset($data['tabs'][0]['config_translation.local_tasks:entity.webform.config_translation_overview']['#link']['title'])) {
-    $data['tabs'][0]['config_translation.local_tasks:entity.webform.config_translation_overview']['#link']['title'] = t('Translate');
+  // Change config entities 'Translate *' tab to be just label 'Translate'.
+  $webform_entities = [
+    'webform',
+    'webform_options',
+  ];
+  foreach ($webform_entities as $webform_entity) {
+    if (isset($data['tabs'][0]["config_translation.local_tasks:entity.$webform_entity.config_translation_overview"]['#link']['title'])) {
+      $data['tabs'][0]["config_translation.local_tasks:entity.$webform_entity.config_translation_overview"]['#link']['title'] = t('Translate');
+    }
   }
+
+  // Change simple config 'Translate *' tab to be just label 'Translate'.
   if (isset($data['tabs'][1]['config_translation.local_tasks:config_translation.item.overview.webform.config'])) {
     $data['tabs'][1]['config_translation.local_tasks:config_translation.item.overview.webform.config']['#link']['title'] = t('Translate');
   }
@@ -213,18 +237,6 @@ function webform_form_alter(&$form, FormStateInterface $form_state, $form_id) {
     // Make sure webform libraries are always attached to submission form.
     _webform_page_attachments($form);
 
-    // Execute hook_form_BASE_FORM_ID_alter for the webform & source entity
-    // without the operation. (i.e. 'add', 'edit', 'notes', etc...)
-    // @see \Drupal\webform\WebformSubmissionForm::getFormId
-    /** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
-    $webform_submission = $form_object->getEntity();
-    $webform = $webform_submission->getWebform();
-
-    $hook = 'form_webform_submission_' . $webform->id() . '_form';
-    $form_id = $form_object->getFormId();
-    \Drupal::service('module_handler')->alter($hook, $form, $form_state, $form_id);
-    \Drupal::service('theme.manager')->alter($hook, $form, $form_state, $form_id);
-
     // After build.
     $form['#after_build'][] = '_webform_form_webform_submission_form_after_build';
   }
@@ -309,7 +321,7 @@ function webform_form_update_manager_update_form_alter(&$form, FormStateInterfac
     '#weight' => -10,
   ];
 
-  // Display warning to backup site when webform  is checked.
+  // Display warning to backup site when webform is checked.
   $form['projects']['#options']['webform']['title']['data'] = [
     'title' => $form['projects']['#options']['webform']['title']['data'],
     'container' => [
@@ -325,6 +337,20 @@ function webform_form_update_manager_update_form_alter(&$form, FormStateInterfac
   ];
 }
 
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function webform_form_views_exposed_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  /** @var \Drupal\views\ViewExecutable $view */
+  $view = $form_state->get('view');
+
+  // Check if this a is webform submission view.
+  // @see \Drupal\webform\WebformSubmissionListBuilder::buildSubmissionViews
+  if (isset($view->webform_submission_view)) {
+    $form['#action'] = Url::fromRoute(\Drupal::routeMatch()->getRouteName(), \Drupal::routeMatch()->getRawParameters()->all())->toString();
+  }
+}
+
 /**
  * Implements hook_system_breadcrumb_alter().
  */
@@ -351,10 +377,50 @@ function webform_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInte
   }
 }
 
+/**
+ * Implements hook_token_info_alter().
+ */
+function webform_token_info_alter(&$data) {
+  module_load_include('inc', 'webform', 'webform.tokens.inc');
+
+  // Append learn more about token suffixes to all webform token descriptions.
+  // @see \Drupal\webform\WebformTokenManager::replace
+  // @see webform_page_attachments()
+  $token_suffixes = t('Append the below suffixes to alter the returned value.') .
+    '<ul>' .
+    '<li>' . t('<code>:clear</code> to removes token when not replaced.') . '</li>' .
+    '<li>' . t('<code>:urlencode</code> URL encodes returned value.') . '</li>' .
+    '<li>' . t('<code>:xmlencode</code> XML encodes returned value.') . '</li>' .
+    '<li>' . t('<code>:htmldecode</code> decodes HTML enities in returned value.') . '<br/><b>' . t('This suffix has security implications.') . '</b><br/>' . t('Use <code>:htmldecode</code> with <code>:striptags</code>.') . '</li>' .
+    '<li>' . t('<code>:striptags</code> removes all HTML tags from returned value.') . '</li>' .
+  '</ul>';
+  $more = _webform_token_render_more(t('Learn about token suffixes'), $token_suffixes);
+  foreach ($data['types'] as $type => &$info) {
+    if (strpos($type, 'webform') === 0) {
+      if (!empty($info['description'])) {
+        $description = $info['description'] . $more;
+      }
+      else {
+        $description = $more;
+      }
+      $info['description'] = Markup::create($description);
+    }
+  }
+}
+
+/**
+ * Implements hook_entity_presave().
+ */
+function webform_entity_presave(EntityInterface $entity) {
+  _webform_clear_webform_submission_list_cache_tag($entity);
+}
+
 /**
  * Implements hook_entity_delete().
  */
 function webform_entity_delete(EntityInterface $entity) {
+  _webform_clear_webform_submission_list_cache_tag($entity);
+
   /** @var \Drupal\webform\WebformEntityReferenceManagerInterface $entity_reference_manager */
   $entity_reference_manager = \Drupal::service('webform.entity_reference_manager');
 
@@ -366,6 +432,27 @@ function webform_entity_delete(EntityInterface $entity) {
   }
 }
 
+/**
+ * Invalidate 'webform_submission_list' cache tag when user or role is updated.
+ *
+ * Once the below issue is resolved we should rework this approach.
+ *
+ * Issue #2811041: Allow views base tables to define additional
+ * cache tags and max age.
+ * https://www.drupal.org/project/drupal/issues/2811041
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ *   An entity.
+ *
+ * @see \Drupal\webform\Entity\WebformSubmission
+ * @see webform_query_webform_submission_access_alter()
+ */
+function _webform_clear_webform_submission_list_cache_tag(EntityInterface $entity) {
+  if (in_array($entity->getEntityTypeId(), ['user', 'user_role'])) {
+    Cache::invalidateTags(['webform_submission_list']);
+  }
+}
+
 /**
  * Implements hook_mail().
  */
@@ -393,11 +480,11 @@ function webform_mail($key, &$message, $params) {
   $message['body'][] = $params['body'];
 
   // Set the header 'From'.
-  // Usingthe 'from_mail' so that the webform's email from value is used
+  // Using the 'from_mail' so that the webform's email from value is used
   // instead of site's email address.
   // @see: \Drupal\Core\Mail\MailManager::mail.
   if (!empty($params['from_mail'])) {
-    $message['from'] = $message['headers']['From'] = (!empty($params['from_name'])) ? Unicode::mimeHeaderEncode($params['from_name'], TRUE) . ' <' . $params['from_mail'] . '>' : $params['from_mail'];
+    $message['from'] = $message['headers']['From'] = (!empty($params['from_name'])) ? Mail::formatDisplayName($params['from_name']) . ' <' . $params['from_mail'] . '>' : $params['from_mail'];
   }
 
   // Set header 'Cc'.
@@ -413,7 +500,6 @@ function webform_mail($key, &$message, $params) {
   // Set header 'Reply-to'.
   $reply_to = $params['reply_to'] ?: '';
   if (empty($reply_to) && !empty($params['from_mail'])) {
-    // @todo Determine if the 'reply-to' must only be the to 'from mail' if 'from mail' has the same domain as the 'site mail'.
     $reply_to = $message['from'];
   }
   if ($reply_to) {
@@ -430,7 +516,7 @@ function webform_mail($key, &$message, $params) {
   $sender_mail = $params['sender_mail'] ?: '';
   $sender_name = $params['sender_name'] ?: $params['from_name'] ?: '';
   if ($sender_mail) {
-    $message['headers']['Sender'] = ($sender_name) ? Unicode::mimeHeaderEncode($sender_name, TRUE) . ' <' . $sender_mail . '>' : $sender_mail;
+    $message['headers']['Sender'] = ($sender_name) ? Mail::formatDisplayName($sender_name) . ' <' . $sender_mail . '>' : $sender_mail;
   }
 }
 
@@ -454,11 +540,11 @@ function webform_mail_alter(&$message) {
  * Implements hook_page_attachments().
  */
 function webform_page_attachments(array &$attachments) {
-  $route_name = Drupal::routeMatch()->getRouteName();
+  $route_name = \Drupal::routeMatch()->getRouteName();
   $url = Url::fromRoute('<current>')->toString();
 
   // Attach global libraries only to webform specific pages.
-  if (preg_match('/^(webform\.|^entity\.([^.]+\.)?webform)/', $route_name) || preg_match('#(/node/add/webform|/admin/help/webform)#', $url)) {
+  if (preg_match('/^(webform\.|^entity\.([^.]+\.)?webform)/', $route_name)) {
     _webform_page_attachments($attachments);
   }
 
@@ -469,7 +555,7 @@ function webform_page_attachments(array &$attachments) {
   }
 
   // Attach 'Contribute' section style.
-  if (\Drupal::routeMatch()->getRouteName() === 'webform.contribute') {
+  if ($route_name === 'webform.contribute') {
     /** @var \Drupal\webform\WebformContributeManagerInterface $contribute_manager */
     $contribute_manager = \Drupal::service('webform.contribute_manager');
     $attachments['#attached']['html_head'][] = [
@@ -481,6 +567,25 @@ function webform_page_attachments(array &$attachments) {
       'webform_contribute',
     ];
   }
+
+  // Attach webform dialog library and options to every page.
+  if (\Drupal::config('webform.settings')->get('settings.dialog')) {
+    $attachments['#attached']['library'][] = 'webform/webform.dialog';
+    $attachments['#attached']['drupalSettings']['webform']['dialog']['options'] = \Drupal::config('webform.settings')->get('settings.dialog_options');
+
+    /** @var \Drupal\webform\WebformRequestInterface $request_handler */
+    $request_handler = \Drupal::service('webform.request');
+    if ($source_entity = $request_handler->getCurrentSourceEntity()) {
+      $attachments['#attached']['drupalSettings']['webform']['dialog']['entity_type'] = $source_entity->getEntityTypeId();
+      $attachments['#attached']['drupalSettings']['webform']['dialog']['entity_id'] = $source_entity->id();
+    }
+  }
+
+  // Attach webform more element to token token help.
+  // @see webform_token_info_alter()
+  if ($route_name === 'help.page' && \Drupal::routeMatch()->getRawParameter('name') === 'token') {
+    $attachments['#attached']['library'][] = 'webform/webform.token';
+  }
 }
 
 /**
@@ -511,6 +616,9 @@ function _webform_page_attachments(array &$attachments) {
     $attachments['#attached']['library'][] = 'webform/webform.element.details.save';
   }
 
+  // Add 'info' message style to all webform pages.
+  $attachments['#attached']['library'][] = 'webform/webform.element.message';
+
   // Assets: Add custom shared and webform specific CSS and JS.
   // @see webform_library_info_build()
   /** @var \Drupal\webform\WebformRequestInterface $request_handler */
@@ -587,13 +695,18 @@ function webform_css_alter(&$css, AttachedAssetsInterface $assets) {
  */
 function webform_js_alter(&$javascript, AttachedAssetsInterface $assets) {
   // Add Google API key required by webform/libraries.jquery.geocomplete
-  // which is dependency for webform/webform.element.location.
-  // @see \Drupal\webform\Element\WebformLocation::processWebformComposite
+  // which is dependency for webform/webform.element.location.geocomplete.
+  //
+  // @see \Drupal\webform\Element\WebformLocationGeocomplete::processWebformComposite
   // @see webform.libraries.yml
+  // @see webform/webform.element.location.geocomplete
+  // @see webform/libraries.jquery.geocomplete
   $settings = $assets->getSettings();
-  if (!empty($settings['webform']['location']['google_maps_api_key'])) {
-    $api_key = $settings['webform']['location']['google_maps_api_key'];
+  if (!empty($settings['webform']['location']['geocomplete']['api_key']) && isset($javascript['https://maps.googleapis.com/maps/api/js?key=API_KEY&libraries=places'])) {
+    $api_key = $settings['webform']['location']['geocomplete']['api_key'];
     $javascript['https://maps.googleapis.com/maps/api/js?key=API_KEY&libraries=places']['data'] = "https://maps.googleapis.com/maps/api/js?key=$api_key&libraries=places";
+    unset($settings['webform']['location']['geocomplete']['api_key']);
+    $assets->setSettings($settings);
   }
 
   _webform_asset_alter($javascript, 'javascript');
@@ -661,6 +774,61 @@ function webform_contextual_links_view_alter(&$element, $items) {
   }
 }
 
+/**
+ * Implements hook_webform_access_rules().
+ */
+function webform_webform_access_rules() {
+  return [
+    'create' => [
+      'title' => t('Create submissions'),
+      'roles' => [
+        'anonymous',
+        'authenticated',
+      ],
+    ],
+    'view_any' => [
+      'title' => t('View any submissions'),
+    ],
+    'update_any' => [
+      'title' => t('Update any submissions'),
+    ],
+    'delete_any' => [
+      'title' => t('Delete any submissions'),
+    ],
+    'purge_any' => [
+      'title' => t('Purge any submissions'),
+    ],
+    'view_own' => [
+      'title' => t('View own submissions'),
+    ],
+    'update_own' => [
+      'title' => t('Update own submissions'),
+    ],
+    'delete_own' => [
+      'title' => t('Delete own submissions'),
+    ],
+    'administer' => [
+      'title' => t('Administer webform & submissions'),
+      'description' => [
+        '#type' => 'webform_message',
+        '#message_type' => 'warning',
+        '#message_message' => t('<strong>Warning</strong>: The below settings give users, permissions, and roles full access to this webform and its submissions.'),
+      ],
+    ],
+    'test' => [
+      'title' => t('Test webform'),
+    ],
+    'configuration' => [
+      'title' => t('Access webform configuration'),
+      'description' => [
+        '#type' => 'webform_message',
+        '#message_type' => 'warning',
+        '#message_message' => t("<strong>Warning</strong>: The below settings give users, permissions, and roles full access to this webform's configuration via API requests."),
+      ],
+    ],
+  ];
+}
+
 /**
  * Adds JavaScript to change the state of an element based on another element.
  *
@@ -711,6 +879,10 @@ function webform_element_info_alter(array &$info) {
  *   The processed element.
  */
 function webform_process_options(&$element, FormStateInterface $form_state, &$complete_form) {
+  if (!WebformElementHelper::isWebformElement($element)) {
+    return $element;
+  }
+
   if (!empty($element['#options_description_display'])) {
     $description_property_name = ($element['#options_description_display'] == 'help') ? '#help' : '#description';
     foreach (Element::children($element) as $key) {
@@ -722,9 +894,22 @@ function webform_process_options(&$element, FormStateInterface $form_state, &$co
 
       list($title, $description) = explode(WebformOptionsHelper::DESCRIPTION_DELIMITER, $title);
       $element[$key]['#title'] = $title;
+      $element[$key]['#webform_element'] = TRUE;
       $element[$key][$description_property_name] = $description;
     }
   }
+
+  // Issue #2839344: Some aria-describedby refers to not existing element ID.
+  // @see https://www.drupal.org/project/drupal/issues/2839344
+  if (!empty($element['#attributes']['aria-describedby'])) {
+    foreach (Element::children($element) as $key) {
+      if (empty($element[$key]['#attributes']['aria-describedby'])
+        && $element['#attributes']['aria-describedby'] === $element[$key]['#attributes']['aria-describedby']) {
+        unset($element[$key]['#attributes']['aria-describedby']);
+      }
+    }
+  }
+
   return $element;
 }
 
@@ -784,21 +969,21 @@ function _webform_entity_element_validate_rendering_exception_handler($exception
   throw $exception;
 }
 
+/******************************************************************************/
+// Query alter functions.
+/******************************************************************************/
+
 /**
- * Implements hook_query_alter().
+ * Implements hook_query_TAG_alter().
  *
  * Append EAV sort to webform_submission entity query.
  *
  * @see http://stackoverflow.com/questions/12893314/sorting-eav-database
  * @see \Drupal\webform\WebformSubmissionListBuilder::getEntityIds
  */
-function webform_query_alter(AlterableInterface $query) {
+function webform_query_webform_submission_list_builder_alter(AlterableInterface $query) {
   /** @var \Drupal\Core\Database\Query\SelectInterface $query */
   $name = $query->getMetaData('webform_submission_element_name');
-  if (!$name) {
-    return;
-  }
-
   $direction = $query->getMetaData('webform_submission_element_direction');
   $property_name = $query->getMetaData('webform_submission_element_property_name');
 
@@ -811,3 +996,228 @@ function webform_query_alter(AlterableInterface $query) {
   }
   $query->orderBy('value', $direction);
 }
+
+/**
+ * Implements hook_query_TAG_alter().
+ */
+function webform_query_entity_reference_alter(AlterableInterface $query) {
+  /** @var \Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection $handler */
+  $handler = $query->getMetaData('entity_reference_selection_handler');
+
+  // Get webform settings used to limit and randomize results.
+  // @see \Drupal\webform\Plugin\WebformElement\WebformEntityReferenceTrait::getTestValues
+  // @see \Drupal\webform\Plugin\WebformElement\WebformEntityReferenceTrait::setOptions
+  // @see \Drupal\webform\Element\WebformEntityTrait::setOptions
+  $configuration = $handler->getConfiguration() + ['_webform_settings' => []];
+  $settings = $configuration['_webform_settings'];
+  if (!empty($settings['random'])) {
+    $query->orderRandom();
+  }
+  if (!empty($settings['limit'])) {
+    $query->range(0, $settings['limit']);
+  }
+}
+
+/**
+ * Implements hook_query_TAG_alter().
+ *
+ * This hook implementation adds webform submission access checks for the
+ * account stored in the 'account' meta-data (or current user if not provided),
+ * for an operation stored in the 'op' meta-data (or 'view' if not provided).
+ */
+function webform_query_webform_submission_access_alter(AlterableInterface $query) {
+  /** @var \Drupal\Core\Database\Query\SelectInterface $query */
+  $op = $query->getMetaData('op') ?: 'view';
+  $account = $query->getMetaData('account') ?: \Drupal::currentUser();
+
+  $entity_type = \Drupal::entityTypeManager()->getDefinition('webform_submission');
+
+  // Get webform submission tables which are used to build the alter query.
+  $webform_submission_tables = [];
+  foreach ($query->getTables() as $table) {
+    if (is_string($table['table']) && $table['table'] === $entity_type->getBaseTable()) {
+      $webform_submission_tables[] = [
+        'alias' => $table['alias'],
+        'condition' => $query->orConditionGroup(),
+      ];
+    }
+  }
+
+  // If there are no webform submission tables then nothing needs to be altered.
+  if (empty($webform_submission_tables)) {
+    return;
+  }
+
+  // If the user has administer access then exit.
+  if ($account->hasPermission('administer webform submission')
+    || $account->hasPermission('administer webform')) {
+    return;
+  }
+
+  // Apply operation specific any and own permissions.
+  if (in_array($op, ['view', 'edit', 'delete'])) {
+    $permission_any = "$op any webform submission";
+    $permission_own = "$op own webform submission";
+
+    // If the user has any permission the query does not have to be altered.
+    if ($account->hasPermission($permission_any)) {
+      return;
+    }
+
+    // If the user has own permission, then add the account id to all
+    // webform submission tables conditions.
+    if ($account->hasPermission($permission_own)) {
+      foreach ($webform_submission_tables as $table) {
+        $table['condition']->condition($table['alias'] . '.uid', $account->id());
+      }
+    }
+  }
+
+  // Alter query based on update access to all webforms.
+  /** @var \Drupal\webform\WebformInterface[] $webforms */
+  if ($account->isAuthenticated()) {
+    // Get cached list of webforms that the user can update so that we don't
+    // have to continually load every webform.
+    $cached = \Drupal::cache()->get('webform_submission_access__account_update__' . $account->id());
+    if ($cached) {
+      $webform_account_access_update = $cached->data;
+    }
+    else {
+      $webform_account_access_update = [];
+      /** @var \Drupal\webform\WebformInterface[] $webforms */
+      $webforms = Webform::loadMultiple();
+      foreach ($webforms as $webform) {
+        if ($webform->access('update', $account)) {
+          $webform_account_access_update[] = $webform->id();
+        }
+      }
+      \Drupal::cache()->set(
+        'webform_submission_access__account_update__' . $account->id(),
+        $webform_account_access_update,
+        Cache::PERMANENT,
+        ['config:webform_list', 'user:' . $account->id()]
+      );
+    }
+    foreach ($webform_account_access_update as $webform_id) {
+      foreach ($webform_submission_tables as $table) {
+        $table['condition']->condition($table['alias'] . '.webform_id', $webform_id);
+      }
+    }
+  }
+
+  // Alter query based on access rules.
+  /** @var \Drupal\webform\WebformAccessRulesManagerInterface $access_rules_manager */
+  $access_rules_manager = \Drupal::service('webform.access_rules_manager');
+
+  // Get cached webform access rules and cache tags so that we don't have
+  // to continually load every webform.
+  $cached = \Drupal::cache()->get('webform_submission_access__webform_access_rules');
+  if ($cached) {
+    $webform_access_rules = $cached->data;
+  }
+  else {
+    /** @var \Drupal\webform\WebformInterface[] $webforms */
+    $webforms = Webform::loadMultiple();
+    $webform_access_rules = [];
+    foreach ($webforms as $webform_id => $webform) {
+      $webform_access_rules[$webform_id] = $access_rules_manager->getAccessRules($webform) ?: [];
+    }
+    \Drupal::cache()->set(
+      'webform_submission_access__webform_access_rules',
+      $webform_access_rules,
+      Cache::PERMANENT,
+      ['config:webform_list']
+    );
+  }
+
+  foreach ($webform_access_rules as $webform_id => $access_rules) {
+    // Check basic and any access rules and add webform id to all
+    // webform submission tables conditions.
+    if ($access_rules_manager->checkAccessRules($op, $account, $access_rules)
+      || $access_rules_manager->checkAccessRules($op . '_any', $account, $access_rules)) {
+      foreach ($webform_submission_tables as $table) {
+        $table['condition']->condition($table['alias'] . '.webform_id', $webform_id);
+      }
+    }
+    // If the user has own access rules, then add the account id to all
+    // webform submission tables conditions.
+    elseif ($access_rules_manager->checkAccessRules($op . '_own', $account, $access_rules)) {
+      foreach ($webform_submission_tables as $table) {
+        /** @var \Drupal\Core\Database\Query\SelectInterface $query */
+        $condition = $query->andConditionGroup();
+        $condition->condition($table['alias'] . '.uid', $account->id());
+        $condition->condition($table['alias'] . '.webform_id', $webform_id);
+        $table['condition']->condition($condition);
+      }
+    }
+  }
+
+  // Alter query based on webform access group.
+  // @todo Figure out how to move the below code to the webform_access.module.
+  if (\Drupal::moduleHandler()->moduleExists('webform_access')) {
+    // Collect access group ids with 'view_any' or 'administer' permissions.
+    /** @var \Drupal\webform_access\WebformAccessGroupStorageInterface $access_group_storage */
+    $access_group_storage = \Drupal::entityTypeManager()->getStorage('webform_access_group');
+    /** @var \Drupal\webform_access\WebformAccessGroupInterface $access_group */
+    $access_groups = $access_group_storage->loadByEntities(NULL, NULL, $account);
+    $access_any_group_ids = [];
+    $access_own_group_ids = [];
+    foreach ($access_groups as $access_group) {
+      $access_group_permissions = $access_group->get('permissions');
+      $access_group_permissions = array_combine($access_group_permissions, $access_group_permissions);
+      if (isset($access_group_permissions['view_any']) || isset($access_group_permissions['administer'])) {
+        $access_any_group_ids[] = $access_group->id();
+      }
+      elseif (isset($access_group_permissions['view_own'])) {
+        $access_own_group_ids[] = $access_group->id();
+      }
+    }
+    if ($access_any_group_ids) {
+      // Add access group entity type, entity id, and webform id to the query.
+      $result = \Drupal::database()->select('webform_access_group_entity', 'ge')
+        ->fields('ge', ['entity_type', 'entity_id', 'webform_id'])
+        ->condition('group_id', $access_any_group_ids, 'IN')
+        ->execute();
+      while ($record = $result->fetchAssoc()) {
+        foreach ($webform_submission_tables as $table) {
+          /** @var \Drupal\Core\Database\Query\SelectInterface $query */
+          $condition = $query->andConditionGroup();
+          $condition->condition($table['alias'] . '.entity_type', $record['entity_type']);
+          $condition->condition($table['alias'] . '.entity_id', (string) $record['entity_id']);
+          $condition->condition($table['alias'] . '.webform_id', $record['webform_id']);
+          $table['condition']->condition($condition);
+        }
+      }
+    }
+    if ($access_own_group_ids) {
+      // Add access group entity type, entity id, and webform id to the query.
+      $result = \Drupal::database()->select('webform_access_group_entity', 'ge')
+        ->fields('ge', ['entity_type', 'entity_id', 'webform_id'])
+        ->condition('group_id', $access_own_group_ids, 'IN')
+        ->execute();
+      while ($record = $result->fetchAssoc()) {
+        foreach ($webform_submission_tables as $table) {
+          /** @var \Drupal\Core\Database\Query\SelectInterface $query */
+          $condition = $query->andConditionGroup();
+          $condition->condition($table['alias'] . '.uid', $account->id());
+          $condition->condition($table['alias'] . '.entity_type', $record['entity_type']);
+          $condition->condition($table['alias'] . '.entity_id', (string) $record['entity_id']);
+          $condition->condition($table['alias'] . '.webform_id', $record['webform_id']);
+          $table['condition']->condition($condition);
+        }
+      }
+    }
+
+  }
+
+  // Apply webform submission table conditions to query.
+  foreach ($webform_submission_tables as $table) {
+    // If a webform submission table does not have any conditions,
+    // we have to block access to the table.
+    if (count($table['condition']->conditions()) === 1) {
+      $table['condition']->where('1 = 0');
+    }
+
+    $query->condition($table['condition']);
+  }
+}
diff --git a/web/modules/webform/webform.permissions.yml b/web/modules/webform/webform.permissions.yml
index 5b311d3b168cbd9b7c8f837e0f130c3a027be4ff..42420c8e808055cc77c6d8eebfdf46add53e5c93 100644
--- a/web/modules/webform/webform.permissions.yml
+++ b/web/modules/webform/webform.permissions.yml
@@ -1,9 +1,9 @@
 'access webform overview':
   title: 'Access the webform overview page'
   description: 'Get an overview of all webforms.'
-'access webform submission log':
-  title: 'Access webform submission log'
-  description: 'Allows viewing of <em>all</em> submission events, if the user can access a webform''s results.'
+'access webform submission user':
+  title: 'Access the webform user submission page'
+  description: 'Allows a user to view their submissions via ''Submissions'' tab on their profile page.'
 
 'administer webform':
   title: 'Administer webforms'
@@ -18,9 +18,14 @@
   title: 'Administer webform element access'
   description: 'Restrict webform element access to certain roles and users.'
 
+'edit webform source':
+  title: 'Edit webform source code'
+  description: 'Editing webform source code allows users to alter and possibly break a webform''s render array.'
+  restrict access: true
+
 'edit webform twig':
   title: 'Edit webform Twig templates'
-  description: 'Editing inline Twig template allows users access any data exposed by Twig functions.'
+  description: 'Editing inline Twig template allows users to access any data exposed by Twig functions.'
   restrict access: true
 
 'edit webform assets':
@@ -43,21 +48,35 @@
 'delete own webform':
   title: 'Delete own webform'
 
+'access own webform configuration':
+  title: 'Access own webform configuration'
+  description: 'Allows users and applications to access a webform''s configuration created by the user.'
+  restrict access: true
+  warning: 'Warning: Give to trusted roles only; this permission has security implications. Note: A webform''s configuration includes all settings and handlers which can contain sensitive information.'
+'access any webform configuration':
+  title: 'Access any webform configuration'
+  description: 'Allows users and applications to access any webform''s configuration.'
+  restrict access: true
+  warning: 'Warning: Give to trusted roles only; this permission has security implications. Note: A webform''s configuration includes all settings and handlers which can contain sensitive information.'
+
 'view any webform submission':
   title: 'View any webform submission'
   description: 'Allows viewing <em>all</em> submissions.'
 'view own webform submission':
   title: 'View own webform submission'
-  description: 'Allows viewing <em>own</em> submissions.'
+  description: 'Allows viewing <em>own</em> submissions for all webforms.'
+  warning: 'Warning: This permission affects access to all webform. Note: To allow users to edit own submissions for an individual webform, please go to the webform''s ''Access'' tab.'
 'edit any webform submission':
   title: 'Edit any webform submission'
   description: 'Allows updating <em>all</em> submissions.'
 'edit own webform submission':
   title: 'Edit own webform submission'
-  description: 'Allows updating <em>own</em> submissions.'
+  description: 'Allows updating <em>own</em> submissions for all webforms.'
+  warning: 'Warning: This permission affects access to all webform. Note: To allow users to update own submissions for an individual webform, please go to the webform''s ''Access'' tab.'
 'delete any webform submission':
-  title: 'Deleting any webform submission'
+  title: 'Delete any webform submission'
   description: 'Allows deleting <em>all</em> submissions.'
 'delete own webform submission':
   title: 'Delete own webform submission'
-  description: 'Allows deleting <em>own</em> submissions.'
+  description: 'Allows deleting <em>own</em> submissions for all webforms.'
+  warning: 'Warning: This permission affects access to all webform. Note: To allow users to deleted own submissions for an individual webform, please go to the webform''s ''Access'' tab.'
diff --git a/web/modules/webform/webform.routing.yml b/web/modules/webform/webform.routing.yml
index 5e9ce44cd05cb19a993f4148a67e9308b6e0c7c5..88f6a1faa6ba5d3dabb830a988b07d7bac0320c5 100644
--- a/web/modules/webform/webform.routing.yml
+++ b/web/modules/webform/webform.routing.yml
@@ -56,15 +56,6 @@ webform.config.advanced:
   requirements:
     _permission: 'administer webform'
 
-# Help
-
-webform.help.video:
-  path: '/admin/help/webform/video/{id}'
-  defaults:
-    _form: '\Drupal\webform\Form\WebformHelpVideoForm'
-  requirements:
-    _permission: 'access content'
-
 # Add-ons
 
 webform.addons:
@@ -75,6 +66,24 @@ webform.addons:
   requirements:
     _permission: 'administer webform'
 
+# Help
+
+# webform.help is dynamically added.
+# @see \Drupal\webform\Routing\WebformRouteSubscriber
+webform.help:
+  path: '/admin/structure/webform/help'
+  defaults:
+    _controller: '\Drupal\webform\Controller\WebformHelpController::index'
+  requirements:
+    _permission: 'access content'
+
+webform.help.video:
+  path: '/admin/help/webform/video/{id}'
+  defaults:
+    _form: '\Drupal\webform\Form\WebformHelpVideoForm'
+  requirements:
+    _permission: 'access content'
+
 # Contribute
 
 webform.contribute:
@@ -117,6 +126,14 @@ entity.webform.autocomplete:
   requirements:
     _custom_access: '\Drupal\webform\Access\WebformAccountAccess::checkOverviewAccess'
 
+entity.webform.autocomplete.archived:
+  path: '/admin/structure/webform/autocomplete/archived'
+  defaults:
+    _controller: '\Drupal\webform\Controller\WebformEntityController::autocomplete'
+    archived: TRUE
+  requirements:
+    _custom_access: '\Drupal\webform\Access\WebformAccountAccess::checkOverviewAccess'
+
 entity.webform.canonical:
   path: '/webform/{webform}'
   defaults:
@@ -125,6 +142,15 @@ entity.webform.canonical:
   requirements:
     _entity_access: 'webform.submission_page'
 
+entity.webform.access_denied:
+  path: '/webform/{webform}/access-denied'
+  defaults:
+    _controller: '\Drupal\webform\Controller\WebformEntityController::accessDenied'
+    _title_callback: '\Drupal\webform\Controller\WebformEntityController::accessDeniedTitle'
+  requirements:
+    # Access denied is available to all users.
+    _access: 'TRUE'
+
 entity.webform.assets.javascript:
   path: '/webform/javascript/{webform}'
   defaults:
@@ -145,13 +171,14 @@ entity.webform.confirmation:
     _controller: '\Drupal\webform\Controller\WebformEntityController::confirmation'
     _title_callback: '\Drupal\webform\Controller\WebformEntityController::title'
   requirements:
-    _entity_access: 'webform.submission_create'
+    _entity_access: 'webform.view'
 
 entity.webform.user.submissions:
-  path: '/webform/{webform}/submissions'
+  path: '/webform/{webform}/submissions/{submission_view}'
   defaults:
     _entity_list: 'webform_submission'
     _title: 'Submissions'
+    submission_view: ''
   options:
     parameters:
       webform:
@@ -160,16 +187,18 @@ entity.webform.user.submissions:
     _entity_access: 'webform.submission_view_own'
 
 entity.webform.user.drafts:
-  path: '/webform/{webform}/drafts'
+  path: '/webform/{webform}/drafts/{submission_view}'
   defaults:
     _entity_list: 'webform_submission'
     _title: 'Drafts'
+    submission_view: ''
   options:
     parameters:
       webform:
         type: 'entity:webform'
   requirements:
-    _entity_access: 'webform.view'
+    _entity_access: 'webform.submission_create'
+    _custom_access: '\Drupal\webform\Access\WebformEntityAccess::checkDraftsAccess'
 
 entity.webform.user.submission:
   path: '/webform/{webform}/submissions/{webform_submission}'
@@ -196,6 +225,22 @@ entity.webform.user.submission.delete:
   requirements:
     _entity_access: 'webform_submission.delete'
 
+entity.webform.user.submission.duplicate:
+  path: '/webform/{webform}/submissions/{webform_submission}/duplicate'
+  defaults:
+    _entity_form: 'webform_submission.duplicate'
+    _title_callback: '\Drupal\webform\Controller\WebformSubmissionViewController::title'
+    duplicate: TRUE
+    setting: 'submission_user_duplicate'
+    value: TRUE
+  options:
+    parameters:
+      webform:
+        type: 'entity:webform'
+  requirements:
+    _entity_access: 'webform_submission.update'
+    _custom_access: '\Drupal\webform\Access\WebformEntityAccess::checkWebformSettingValue'
+
 entity.webform.test_form:
   path: '/webform/{webform}/test'
   defaults:
@@ -208,6 +253,14 @@ entity.webform.test_form:
   requirements:
     _entity_access: 'webform.test'
 
+entity.webform.export_form:
+  path: '/admin/structure/webform/manage/{webform}/export'
+  defaults:
+    _entity_form: 'webform.export'
+    _title_callback: '\Drupal\webform\Controller\WebformEntityController::title'
+  requirements:
+    _permission: 'export configuration'
+
 entity.webform.add_form:
   path: '/admin/structure/webform/add'
   defaults:
@@ -293,24 +346,24 @@ entity.webform.settings_access:
 
 # Webform submission results
 
-entity.webform.results_user:
-  path: '/admin/structure/webform/manage/{webform}/results/user'
+entity.webform.results_submissions:
+  path: '/admin/structure/webform/manage/{webform}/results/submissions/{submission_view}'
   defaults:
     _entity_list: 'webform_submission'
-    _title: 'Webform results'
+    _title_callback: '\Drupal\webform\Controller\WebformEntityController::title'
+    submission_view: ''
   options:
     parameters:
       webform:
         type: 'entity:webform'
   requirements:
-    _entity_access: 'webform.submission_view_own'
+    _entity_access: 'webform.submission_view_any'
     _custom_access: '\Drupal\webform\Access\WebformEntityAccess:checkResultsAccess'
 
-entity.webform.results_submissions:
-  path: '/admin/structure/webform/manage/{webform}/results/submissions'
+entity.webform.results.source_entity.autocomplete:
+  path: '/admin/structure/webform/manage/{webform}/results/source-entity/autocomplete'
   defaults:
-    _entity_list: 'webform_submission'
-    _title_callback: '\Drupal\webform\Controller\WebformEntityController::title'
+    _controller: '\Drupal\webform\Controller\WebformSubmissionsController::sourceEntityAutocomplete'
   options:
     parameters:
       webform:
@@ -371,20 +424,6 @@ entity.webform.results_clear:
     _entity_access: 'webform.submission_purge_any'
     _custom_access: '\Drupal\webform\Access\WebformEntityAccess:checkResultsAccess'
 
-entity.webform.results_log:
-  path: '/admin/structure/webform/manage/{webform}/results/log'
-  defaults:
-    _controller: '\Drupal\webform\Controller\WebformSubmissionLogController::overview'
-    _title_callback: '\Drupal\webform\Controller\WebformEntityController::title'
-  options:
-    parameters:
-      webform:
-        type: 'entity:webform'
-  requirements:
-    _permission: 'access webform submission log'
-    _entity_access: 'webform.submission_view_any'
-    _custom_access: '\Drupal\webform\Access\WebformEntityAccess:checkLogAccess'
-
 # Webform options
 
 entity.webform_options.collection:
@@ -395,6 +434,13 @@ entity.webform_options.collection:
   requirements:
     _permission: 'administer webform'
 
+entity.webform_options.autocomplete:
+  path: '/admin/structure/webform/config/options/autocomplete'
+  defaults:
+    _controller: '\Drupal\webform\Controller\WebformOptionsController::autocomplete'
+  requirements:
+    _permission: 'administer webform'
+
 entity.webform_options.add_form:
   path: '/admin/structure/webform/config/options/manage/add'
   defaults:
@@ -430,10 +476,11 @@ entity.webform_options.delete_form:
 # Webform results (submissions)
 
 entity.webform_submission.collection:
-  path: '/admin/structure/webform/submissions/manage'
+  path: '/admin/structure/webform/submissions/manage/{submission_view}'
   defaults:
     _entity_list: 'webform_submission'
     _title: 'Webforms: Submissions'
+    submission_view: ''
   requirements:
     _custom_access: '\Drupal\webform\Access\WebformAccountAccess:checkSubmissionAccess'
 
@@ -445,16 +492,20 @@ entity.webform_submission.collection_purge:
   requirements:
     _permission: 'administer webform'
 
-# Webform log (submissions)
+# Webform user (submissions)
 
-entity.webform_submission.collection_log:
-  path: '/admin/structure/webform/submissions/log'
+entity.webform_submission.user:
+  path: '/user/{user}/submissions/{submission_view}'
   defaults:
-    _controller: '\Drupal\webform\Controller\WebformSubmissionLogController::overview'
-    _title: 'Webforms: Submissions log'
+    _entity_list: 'webform_submission'
+    _title: 'Submissions'
+    submission_view: ''
   requirements:
-    _permission: 'access webform submission log'
-    _custom_access: '\Drupal\webform\Access\WebformAccountAccess:checkSubmissionAccess'
+    _custom_access: '\Drupal\webform\Access\WebformAccountAccess::checkUserSubmissionsAccess'
+  options:
+    parameters:
+      user:
+        type: entity:user
 
 # Webform submissions
 
@@ -467,6 +518,15 @@ entity.webform_submission.canonical:
   requirements:
     _entity_access: 'webform_submission.view'
 
+entity.webform_submission.access_denied:
+  path: '/admin/structure/webform/manage/{webform}/submission/{webform_submission}/access-denied'
+  defaults:
+    _controller: '\Drupal\webform\Controller\WebformSubmissionController::accessDenied'
+    _title_callback: '\Drupal\webform\Controller\WebformSubmissionController::accessDeniedTitle'
+  requirements:
+    # Access denied is available to all users.
+    _access: 'TRUE'
+
 entity.webform_submission.table:
   path: '/admin/structure/webform/manage/{webform}/submission/{webform_submission}/table'
   defaults:
@@ -492,18 +552,9 @@ entity.webform_submission.yaml:
     _title_callback: '\Drupal\webform\Controller\WebformSubmissionViewController::title'
     view_mode: 'yaml'
   requirements:
+    _permission: 'edit webform source'
     _entity_access: 'webform_submission.view_any'
 
-entity.webform_submission.log:
-  path: '/admin/structure/webform/manage/{webform}/submission/{webform_submission}/log'
-  defaults:
-    _controller: '\Drupal\webform\Controller\WebformSubmissionLogController::overview'
-    _title_callback: '\Drupal\webform\Controller\WebformSubmissionViewController::title'
-  requirements:
-    _permission: 'access webform submission log'
-    _entity_access: 'webform_submission.view_any'
-    _custom_access: '\Drupal\webform\Access\WebformEntityAccess::checkLogAccess'
-
 entity.webform_submission.edit_form:
   path: '/admin/structure/webform/manage/{webform}/submission/{webform_submission}/edit'
   defaults:
@@ -535,6 +586,7 @@ entity.webform_submission.resend_form:
     _form: 'Drupal\webform\Form\WebformSubmissionResendForm'
     _title_callback: '\Drupal\webform\Controller\WebformSubmissionViewController::title'
   requirements:
+    _entity_access: 'webform_submission.update_any'
     _custom_access: '\Drupal\webform\Access\WebformSubmissionAccess::checkResendAccess'
 
 entity.webform_submission.duplicate_form:
@@ -594,6 +646,7 @@ entity.webform.handler:
     _title: 'Select a handler'
   requirements:
     _entity_access: 'webform.update'
+    _custom_access: '\Drupal\webform\Access\WebformHandlerAccess::checkHandlerAccess'
 
 entity.webform.handler.add_form:
   path: '/admin/structure/webform/manage/{webform}/handlers/add/{webform_handler}'
@@ -602,6 +655,17 @@ entity.webform.handler.add_form:
     _title: 'Add webform handler'
   requirements:
     _entity_access: 'webform.update'
+    _custom_access: '\Drupal\webform\Access\WebformHandlerAccess::checkHandlerAccess'
+
+entity.webform.handler.add_email:
+  path: '/admin/structure/webform/manage/{webform}/handlers/add/email'
+  defaults:
+    _form: '\Drupal\webform\Form\WebformHandlerAddForm'
+    _title: 'Add email'
+    webform_handler: email
+  requirements:
+    _entity_access: 'webform.update'
+    _custom_access: '\Drupal\webform\Access\WebformHandlerAccess::checkHandlerAccess'
 
 entity.webform.handler.edit_form:
   path: '/admin/structure/webform/manage/{webform}/handlers/{webform_handler}/edit'
@@ -627,29 +691,47 @@ entity.webform.handler.delete_form:
   requirements:
     _entity_access: 'webform.update'
 
+entity.webform.handler.enable:
+  path: '/admin/structure/webform/manage/{webform}/handlers/{webform_handler}/enable'
+  defaults:
+    _controller: '\Drupal\webform\WebformEntityHandlersForm::ajaxOperation'
+    op: enable
+  requirements:
+    _entity_access: 'webform.update'
+    _csrf_token: 'TRUE'
+
+entity.webform.handler.disable:
+  path: '/admin/structure/webform/manage/{webform}/handlers/{webform_handler}/disable'
+  defaults:
+    _controller: '\Drupal\webform\WebformEntityHandlersForm::ajaxOperation'
+    op: disable
+  requirements:
+    _entity_access: 'webform.update'
+    _csrf_token: 'TRUE'
+
 # Plugins.
 
-webform.element_plugins:
-  path: '/admin/structure/webform/plugins/elements'
+webform.reports_plugins.elements:
+  path: '/admin/reports/webform-plugins/elements'
   defaults:
     _controller: '\Drupal\webform\Controller\WebformPluginElementController::index'
-    _title: 'Webforms: Element plugins'
+    _title: 'Webform plugins: Elements'
   requirements:
     _permission: 'administer webform'
 
-webform.handler_plugins:
-  path: '/admin/structure/webform/plugins/handlers'
+webform.reports_plugins.handlers:
+  path: '/admin/reports/webform-plugins/handlers'
   defaults:
     _controller: '\Drupal\webform\Controller\WebformPluginHandlerController::index'
-    _title: 'Webforms: Handler plugins'
+    _title: 'Webform plugins: Handlers'
   requirements:
     _permission: 'administer webform'
 
-webform.exporter_plugins:
-  path: '/admin/structure/webform/plugins/exporters'
+webform.reports_plugins.exporters:
+  path: '/admin/reports/webform-plugins/exporters'
   defaults:
     _controller: '\Drupal\webform\Controller\WebformPluginExporterController::index'
-    _title: 'Webforms: Export plugins'
+    _title: 'Webform plugins: Exporters'
   requirements:
     _permission: 'administer webform'
 
diff --git a/web/modules/webform/webform.services.yml b/web/modules/webform/webform.services.yml
index 07f4f4c0f0fda2276cef293849a5ed4ce5f6d6b6..180ccd22292184a43982410fca66ee18daea4a15 100644
--- a/web/modules/webform/webform.services.yml
+++ b/web/modules/webform/webform.services.yml
@@ -25,6 +25,11 @@ services:
     factory: logger.factory:get
     arguments: ['webform']
 
+  logger.channel.webform_submission:
+    class: Drupal\Core\Logger\LoggerChannel
+    factory: logger.factory:get
+    arguments: ['webform_submission']
+
   # Services.
 
   webform.addons_manager:
@@ -59,11 +64,11 @@ services:
 
   webform.message_manager:
     class: Drupal\webform\WebformMessageManager
-    arguments: ['@current_user', '@config.factory', '@entity_type.manager', '@logger.channel.webform', '@renderer', '@webform.request', '@webform.token_manager']
+    arguments: ['@current_user', '@config.factory', '@entity_type.manager', '@logger.channel.webform', '@renderer', '@messenger', '@webform.request', '@webform.token_manager']
 
   webform.translation_manager:
     class: Drupal\webform\WebformTranslationManager
-    arguments: ['@language_manager', '@config.factory', '@plugin.manager.webform.element']
+    arguments: ['@current_route_match', '@language_manager', '@config.factory', '@messenger', '@plugin.manager.webform.element']
 
   webform.request:
     class: Drupal\webform\WebformRequest
@@ -75,7 +80,7 @@ services:
 
   webform_submission.exporter:
     class: Drupal\webform\WebformSubmissionExporter
-    arguments: ['@config.factory', '@file_system', '@entity_type.manager', '@stream_wrapper_manager', '@plugin.manager.webform.element', '@plugin.manager.webform.exporter']
+    arguments: ['@config.factory', '@file_system', '@entity_type.manager', '@stream_wrapper_manager', '@plugin.manager.archiver', '@plugin.manager.webform.element', '@plugin.manager.webform.exporter']
 
   webform.third_party_settings_manager:
     class: Drupal\webform\WebformThirdPartySettingsManager
@@ -83,11 +88,15 @@ services:
 
   webform.token_manager:
     class: Drupal\webform\WebformTokenManager
-    arguments: ['@config.factory', '@module_handler', '@theme.manager', '@token']
+    arguments: ['@current_user', '@language_manager', '@config.factory', '@module_handler', '@token']
 
   webform.theme_manager:
     class: Drupal\webform\WebformThemeManager
-    arguments: ['@config.factory', '@renderer', '@theme.manager', '@theme.initialization']
+    arguments: ['@config.factory', '@renderer', '@theme.manager', '@theme_handler', '@theme.initialization']
+
+  webform.access_rules_manager:
+    class: Drupal\webform\WebformAccessRulesManager
+    arguments: ['@module_handler']
 
   webform_submission.conditions_validator:
     class: Drupal\webform\WebformSubmissionConditionsValidator
@@ -103,9 +112,9 @@ services:
 
   # Event subscriber.
 
-  webform.event_subscriber:
-    class: Drupal\webform\EventSubscriber\WebformSubscriber
-    arguments: ['@current_user', '@config.factory', '@renderer', '@redirect.destination', '@webform.token_manager']
+  webform.exception_html_subscriber:
+    class: Drupal\webform\EventSubscriber\WebformExceptionHtmlSubscriber
+    arguments: ['@http_kernel', '@logger.channel.php', '@redirect.destination', '@router.no_access_checks', '@current_user', '@config.factory', '@renderer', '@messenger', '@webform.token_manager']
     tags:
       - { name: event_subscriber }
 
diff --git a/web/modules/webform/webform.tokens.inc b/web/modules/webform/webform.tokens.inc
index 93a5db780a879fc29d21950ccbf32f66b280285f..6cccecbef9c9c01ba0b9f83fac81b6c62d416d00 100644
--- a/web/modules/webform/webform.tokens.inc
+++ b/web/modules/webform/webform.tokens.inc
@@ -11,11 +11,12 @@
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Render\Markup;
 use Drupal\user\Entity\User;
-use Drupal\webform\Plugin\WebformElement\WebformComputedBase;
-use Drupal\webform\Utility\WebformDateHelper;
 use Drupal\webform\Plugin\WebformElementManagerInterface;
 use Drupal\webform\Plugin\WebformElementEntityReferenceInterface;
+use Drupal\webform\Plugin\WebformElement\WebformComputedBase;
+use Drupal\webform\Utility\WebformDateHelper;
 use Drupal\webform\WebformSubmissionInterface;
+use Drupal\Core\Url;
 
 /**
  * Implements hook_token_info().
@@ -24,16 +25,6 @@ function webform_token_info() {
   $types = [];
   $tokens = [];
 
-  /****************************************************************************/
-  // Authenticated user.
-  /****************************************************************************/
-
-  $types['webform-authenticated-user'] = [
-    'name' => t('Webform authenticated user'),
-    'description' => t('Tokens related to the currently authenticated user.'),
-    'type' => 'user',
-  ];
-
   /****************************************************************************/
   // Webform submission.
   /****************************************************************************/
@@ -93,7 +84,7 @@ function webform_token_info() {
     'name' => t('In draft'),
     'description' => t('Is the webform submission in draft.'),
   ];
-  $webform['label'] = [
+  $webform_submission['label'] = [
     'name' => t('Label'),
     'description' => t('The label of the webform submission.'),
   ];
@@ -154,35 +145,35 @@ function webform_token_info() {
   ];
   $webform_submission['values'] = [
     'name' => t('Submission values'),
-    'description' => Markup::create((string) t('Webform tokens from submitted data.') .
-      t("Replace the '?' with...") . '<br />' .
-      '<ul>' .
-      '<li>element_key</li>' .
-      '<li>element_key:format:items</li>' .
-      '<li>element_key:delta</li>' .
-      '<li>element_key:sub_element_key</li>' .
-      '<li>element_key:delta:sub_element_key</li>' .
-      '<li>element_key:sub_element_key:format</li>' .
-      '<li>element_key:delta:sub_element_key:format</li>' .
-      '<li>element_key:delta:format</li>' .
-      '<li>element_key:delta:format:html</li>' .
-      '<li>element_key:entity:*</li>' .
-      '<li>element_key:entity:*</li>' .
-      '<li>element_key:delta:entity:*</li>' .
-      '<li>element_key:delta:entity:field_name:*</li>' .
-      '<li>element_key:sub_element_key:entity:*</li>' .
-      '<li>element_key:sub_element_key:entity:field_name:*</li>' .
-      '<li>element_key:delta:sub_element_key:entity:*</li>' .
-      '</ul>' .
-      t("All items after the 'element_key' are optional.") . '<br />' .
-      t("The 'delta' is the numeric index for specific value") . '<br />' .
-      t("The 'sub_element_key' is a composite element's sub element key.") . '<br />' .
-      t("The 'format' can be 'value', 'raw', or custom format specifically associated with the element") . '<br />' .
-      t("The 'items' can be 'comma', 'semicolon', 'and', 'ol', 'ul', or custom delimiter") . '<br />' .
-      t("The 'entity:*' applies to the referenced entity") . '<br />' .
-      t("Add 'html' at the end of the token to return HTML markup instead of plain text.") . '<br />' .
-      t("Add 'clear' at the end of the token to return an empty value if element is missing.") . '<br />' .
-      t("For example, to display the Contact webform's 'Subject' element's value you would use the [webform_submission:values:subject] token.")
+    'description' => Markup::create(t('Webform tokens from submitted data.') .
+      _webform_token_render_more(t('Learn about submission value tokens'),
+        t("Replace the '?' with…") . '<br />' .
+        '<ul>' .
+        '<li>element_key</li>' .
+        '<li>element_key:format:items</li>' .
+        '<li>element_key:delta</li>' .
+        '<li>element_key:sub_element_key</li>' .
+        '<li>element_key:delta:sub_element_key</li>' .
+        '<li>element_key:sub_element_key:format</li>' .
+        '<li>element_key:delta:sub_element_key:format</li>' .
+        '<li>element_key:delta:format</li>' .
+        '<li>element_key:delta:format:html</li>' .
+        '<li>element_key:entity:*</li>' .
+        '<li>element_key:delta:entity:*</li>' .
+        '<li>element_key:delta:entity:field_name:*</li>' .
+        '<li>element_key:sub_element_key:entity:*</li>' .
+        '<li>element_key:sub_element_key:entity:field_name:*</li>' .
+        '<li>element_key:delta:sub_element_key:entity:*</li>' .
+        '</ul>' .
+        t("All items after the 'element_key' are optional.") . '<br />' .
+        t("The 'delta' is the numeric index for specific value") . '<br />' .
+        t("The 'sub_element_key' is a composite element's sub element key.") . '<br />' .
+        t("The 'format' can be 'value', 'raw', or custom format specifically associated with the element") . '<br />' .
+        t("The 'items' can be 'comma', 'semicolon', 'and', 'ol', 'ul', or custom delimiter") . '<br />' .
+        t("The 'entity:*' applies to the referenced entity") . '<br />' .
+        t("Add 'html' at the end of the token to return HTML markup instead of plain text.") . '<br />' .
+        t("For example, to display the Contact webform's 'Subject' element's value you would use the [webform_submission:values:subject] token.")
+      )
     ),
     'dynamic' => TRUE,
   ];
@@ -216,13 +207,23 @@ function webform_token_info() {
     'name' => t('Source entity'),
     'description' => t('The source entity that the webform submission was submitted from.'),
     'type' => 'entity',
+    'dynamic' => TRUE,
   ];
   $webform_submission['submitted-to'] = [
     'name' => t('Submitted to'),
     'description' => t('The source entity or webform that the webform submission was submitted from.'),
     'type' => 'entity',
+    'dynamic' => TRUE,
   ];
 
+  // Append link to token help to source-entity and submitted-to description.
+  if (\Drupal::moduleHandler()->moduleExists('token') && \Drupal::moduleHandler()->moduleExists('help')) {
+    $t_args = [':href' => Url::fromRoute('help.page', ['name' => 'token'])->toString()];
+    $token_help = t('For a list of the currently available source entity related tokens, please see <a href=":href">token help</a>.', $t_args);
+    $webform_submission['source-entity']['description'] = Markup::create($webform_submission['source-entity']['description'] . '<br/>' . $token_help);
+    $webform_submission['submitted-to']['description'] = Markup::create($webform_submission['submitted-to']['description'] . '<br/>' . $token_help);
+  }
+
   $tokens['webform_submission'] = $webform_submission;
 
   /****************************************************************************/
@@ -268,13 +269,15 @@ function webform_token_info() {
   ];
   $webform['handler'] = [
     'name' => t('Handler response'),
-    'description' => Markup::create((string) t('Webform handler response tokens.') . '<br/>' .
-      t("Replace the '?' with...") . '<br />' .
-      '<ul>' .
-      '<li>handler_id:state:key</li>' .
-      '<li>handler_id:state:key1:key2</li>' .
-      '</ul>' .
-      t("For example, to display a remote post's confirmation number you would use the [webform:handler:remote_post:completed:confirmation_number] token.")
+    'description' => Markup::create(t('Webform handler response tokens.') .
+      _webform_token_render_more(t('Learn about handler response tokens'),
+        t("Replace the '?' with…") . '<br />' .
+        '<ul>' .
+        '<li>handler_id:state:key</li>' .
+        '<li>handler_id:state:key1:key2</li>' .
+        '</ul>' .
+        t("For example, to display a remote post's confirmation number you would use the [webform:handler:remote_post:completed:confirmation_number] token.")
+      )
     ),
     'dynamic' => TRUE,
   ];
@@ -289,7 +292,7 @@ function webform_token_info() {
   if ($roles) {
     $types['webform_role'] = [
       'name' => t('Webform roles'),
-      'description' => t("Tokens related to user roles that can receive email. <em>This token is only available to a Webform email handler's 'To', 'CC', and 'BCC' email recipents.</em>"),
+      'description' => t("Tokens related to user roles that can receive email. <em>This token is only available to a Webform email handler's 'To', 'CC', and 'BCC' email recipients.</em>"),
       'needs-data' => 'webform_role',
     ];
 
@@ -330,20 +333,7 @@ function webform_tokens($type, $tokens, array $data, array $options, BubbleableM
   }
 
   $replacements = [];
-  if ($type == 'webform-authenticated-user') {
-    if (\Drupal::currentUser()->isAuthenticated()) {
-      $account = User::load(\Drupal::currentUser()->id());
-      $bubbleable_metadata->addCacheableDependency($account);
-      $replacements += $token_service->generate('user', $tokens, ['user' => $account], $options, $bubbleable_metadata);
-    }
-    // Make sure to clear all tokens.
-    foreach ($tokens as $name => $original) {
-      if (!isset($replacements[$original])) {
-        $replacements[$original] = '';
-      }
-    }
-  }
-  elseif ($type == 'webform_role' && !empty($data['webform_role'])) {
+  if ($type == 'webform_role' && !empty($data['webform_role'])) {
     $roles = $data['webform_role'];
     $any_role = in_array('authenticated', $roles) ? TRUE : FALSE;
     foreach ($tokens as $role_name => $original) {
@@ -390,7 +380,7 @@ function webform_tokens($type, $tokens, array $data, array $options, BubbleableM
     $webform = $webform_submission->getWebform();
     $bubbleable_metadata->addCacheableDependency($webform);
 
-    $source_entity = $webform_submission->getSourceEntity();
+    $source_entity = $webform_submission->getSourceEntity(TRUE);
     if ($source_entity) {
       $bubbleable_metadata->addCacheableDependency($source_entity);
     }
@@ -576,10 +566,10 @@ function webform_tokens($type, $tokens, array $data, array $options, BubbleableM
     if (($webform_tokens = $token_service->findWithPrefix($tokens, 'webform')) && ($webform = $webform_submission->getWebform())) {
       $replacements += $token_service->generate('webform', $webform_tokens, ['webform' => $webform], $options, $bubbleable_metadata);
     }
-    if (($source_entity_tokens = $token_service->findWithPrefix($tokens, 'source-entity')) && ($source_entity = $webform_submission->getSourceEntity())) {
+    if (($source_entity_tokens = $token_service->findWithPrefix($tokens, 'source-entity')) && ($source_entity = $webform_submission->getSourceEntity(TRUE))) {
       $replacements += $token_service->generate($source_entity->getEntityTypeId(), $source_entity_tokens, [$source_entity->getEntityTypeId() => $source_entity], $options, $bubbleable_metadata);
     }
-    if (($submitted_to_tokens = $token_service->findWithPrefix($tokens, 'submitted-to')) && ($submitted_to = $webform_submission->getSourceEntity() ?: $webform_submission->getWebform())) {
+    if (($submitted_to_tokens = $token_service->findWithPrefix($tokens, 'submitted-to')) && ($submitted_to = $webform_submission->getSourceEntity(TRUE) ?: $webform_submission->getWebform())) {
       $replacements += $token_service->generate($submitted_to->getEntityTypeId(), $submitted_to_tokens, [$submitted_to->getEntityTypeId() => $submitted_to], $options, $bubbleable_metadata);
     }
 
@@ -687,6 +677,8 @@ function _webform_token_get_submission_value($value_token, array $options, Webfo
   // [values:{element_key}:{format}:{items}]
   // [values:{element_key}:{format}:html]
   // [values:{element_key}:{format}:{items}:html]
+  // [values:{element_key}:{format}:urlencode]
+  // [values:{element_key}:{format}:{items}:urlencode]
   // [values:{element_key}:{delta}:{format}]
   // [values:{element_key}:{delta}:{sub-element}]
   $keys = explode(':', $value_token);
@@ -698,30 +690,40 @@ function _webform_token_get_submission_value($value_token, array $options, Webfo
     return _webform_token_get_submission_values($options, $webform_submission);
   }
 
+  // Set default options.
+  $options += [
+    'html' => FALSE,
+  ];
+
+  // Parse suffixes and set options.
+  $suffixes = [
+    // Indicates the tokens should be formatted as HTML instead of plain text.
+    'html',
+  ];
+  foreach ($suffixes as $suffix) {
+    if ($keys && in_array($suffix, $keys)) {
+      $keys = array_diff($keys, [$suffix]);
+      $options[$suffix] = TRUE;
+    }
+  }
+
   $element = $webform_submission->getWebform()->getElement($element_key, TRUE);
 
   // Exit if form element does not exist.
   if (!$element) {
-    return (in_array('clear', $keys)) ? '' : NULL;
+    return NULL;
   }
 
   $element_plugin = $element_manager->getElementInstance($element);
 
-  // Get value for a computed element.
+  // Always get value for a computed element.
   if ($element_plugin instanceof WebformComputedBase) {
     return $element_plugin->getValue($element, $webform_submission);
   }
 
   // Exit if no submission data and form element is not a container.
   if (!isset($submission_data[$element_key]) && !$element_plugin->isContainer($element)) {
-    return (in_array('clear', $keys)) ? '' : NULL;
-  }
-
-  // Look for :html which indicates the tokens should be formatted as HTML
-  // instead of plain text.
-  if ($keys && in_array('html', $keys)) {
-    $keys = array_diff($keys, ['html']);
-    $options['html'] = TRUE;
+    return NULL;
   }
 
   // If multiple value element look for delta.
@@ -742,6 +744,10 @@ function _webform_token_get_submission_value($value_token, array $options, Webfo
     $composite_key = NULL;
   }
 
+  /****************************************************************************/
+  // Get value.
+  /****************************************************************************/
+
   // Set entity reference chaining.
   if ($keys && $keys[0] == 'entity' && $element_plugin instanceof WebformElementEntityReferenceInterface) {
     // Remove entity from keys.
@@ -759,12 +765,13 @@ function _webform_token_get_submission_value($value_token, array $options, Webfo
       ];
       $entity_token_name = (isset($entity_token_names[$entity_type])) ? $entity_token_names[$entity_type] : $entity_type;
       $entity_token = implode(':', $keys);
-      return Markup::create(\Drupal::token()->replace(
+      $token_value = Markup::create(\Drupal::token()->replace(
         "[$entity_token_name:$entity_token]",
         [$entity_token_name => $entity],
         $options,
         $bubbleable_metadata
       ));
+      return $token_value;
     }
     else {
       return '';
@@ -792,21 +799,20 @@ function _webform_token_get_submission_value($value_token, array $options, Webfo
   if (is_array($token_value)) {
     // Note, tokens can't include CSS and JS libraries since they will
     // can be included in an email.
-    return \Drupal::service('renderer')->renderPlain($token_value);
-  }
-  elseif ($token_value instanceof MarkupInterface) {
-    return $token_value;
+    $token_value = \Drupal::service('renderer')->renderPlain($token_value);
   }
   elseif (isset($element['#format']) && $element['#format'] === 'raw') {
     // Make sure raw tokens are always rendered AS-IS.
-    return Markup::create((string) $token_value);
+    $token_value = Markup::create((string) $token_value);
   }
-  else {
+  elseif (!($token_value instanceof MarkupInterface)) {
     // All strings will be escaped as HtmlEscapedText.
     // @see \Drupal\Core\Utility\Token::replace
     // @see \Drupal\Component\Render\HtmlEscapedText
-    return (string) $token_value;
+    $token_value = (string) $token_value;
   }
+
+  return $token_value;
 }
 
 /**
@@ -821,12 +827,45 @@ function _webform_token_get_submission_value($value_token, array $options, Webfo
  *   Webform submission values.
  */
 function _webform_token_get_submission_values(array $options, WebformSubmissionInterface $webform_submission) {
+  static $rendering;
+  if ($rendering) {
+    $token = (!empty($options['html'])) ? '[webform_submission:values:html]' : '[webform_submission:values]';
+    throw new \LogicException("Recursive rendering of $token detected.");
+  }
+
+  $rendering = TRUE;
+
   $submission_format = (!empty($options['html'])) ? 'html' : 'text';
   /** @var \Drupal\webform\WebformSubmissionViewBuilderInterface $view_builder */
   $view_builder = \Drupal::entityTypeManager()->getViewBuilder('webform_submission');
   $form_elements = $webform_submission->getWebform()->getElementsInitialized();
   $token_value = $view_builder->buildElements($form_elements, $webform_submission, $options, $submission_format);
+
   // Note, tokens can't include CSS and JS libraries since they can be
   // included in an email.
-  return \Drupal::service('renderer')->renderPlain($token_value);
+  $value = \Drupal::service('renderer')->renderPlain($token_value);
+
+  $rendering = FALSE;
+
+  return $value;
+}
+
+/**
+ * Render webform more element (slideouts) for token descriptions.
+ *
+ * @param string $more_title
+ *   More title.
+ * @param string $more
+ *   More content.
+ *
+ * @return string
+ *   Rendered webform more element.
+ */
+function _webform_token_render_more($more_title, $more) {
+  $build = [
+    '#type' => 'webform_more',
+    '#more' => $more,
+    '#more_title' => $more_title,
+  ];
+  return (string) \Drupal::service('renderer')->renderPlain($build);
 }