summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--composer.json12
-rw-r--r--composer.lock191
-rw-r--r--composer/Metapackage/CoreRecommended/composer.json9
-rw-r--r--composer/Metapackage/DevDependencies/composer.json12
-rw-r--r--composer/Metapackage/PinnedDevDependencies/composer.json8
-rw-r--r--core/.phpstan-baseline.php6
-rw-r--r--core/composer.json3
-rw-r--r--core/lib/Drupal.php2
-rw-r--r--core/modules/contextual/contextual.libraries.yml20
-rw-r--r--core/modules/contextual/css/contextual.theme.css4
-rw-r--r--core/modules/contextual/js/contextual.js99
-rw-r--r--core/modules/contextual/js/contextual.toolbar.js35
-rw-r--r--core/modules/contextual/js/contextualModelView.js254
-rw-r--r--core/modules/contextual/js/models/StateModel.js130
-rw-r--r--core/modules/contextual/js/toolbar/contextualToolbarModelView.js175
-rw-r--r--core/modules/contextual/js/toolbar/models/StateModel.js126
-rw-r--r--core/modules/contextual/js/toolbar/views/AuralView.js122
-rw-r--r--core/modules/contextual/js/toolbar/views/VisualView.js85
-rw-r--r--core/modules/contextual/js/views/AuralView.js59
-rw-r--r--core/modules/contextual/js/views/KeyboardView.js62
-rw-r--r--core/modules/contextual/js/views/RegionView.js75
-rw-r--r--core/modules/contextual/js/views/VisualView.js109
-rw-r--r--core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php75
-rw-r--r--core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php2
-rw-r--r--core/modules/package_manager/tests/src/Build/TemplateProjectTestBase.php2
-rw-r--r--core/modules/toolbar/js/views/ToolbarVisualView.js4
-rw-r--r--core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarIntegrationTest.php52
-rw-r--r--core/profiles/demo_umami/tests/src/FunctionalJavascript/AssetAggregationAcrossPagesTest.php2
-rw-r--r--core/themes/claro/css/classy/components/tablesort.css3
-rw-r--r--core/themes/claro/css/components/dialog.css3
-rw-r--r--core/themes/claro/css/components/dialog.pcss.css3
-rw-r--r--core/themes/claro/images/icons/buttonText/ex.svg1
-rw-r--r--core/themes/olivero/olivero.libraries.yml16
-rw-r--r--core/themes/olivero/olivero.theme9
34 files changed, 753 insertions, 1017 deletions
diff --git a/composer.json b/composer.json
index afccd14e1d2f..c9656c5ddcd4 100644
--- a/composer.json
+++ b/composer.json
@@ -35,12 +35,12 @@
"phpstan/phpstan": "^1.12.4 || ^2.1.14",
"phpstan/phpstan-phpunit": "^1.3.16 || ^2.0.6",
"phpunit/phpunit": "^10.5.19 || ^11.5.3",
- "symfony/browser-kit": "^7.2",
- "symfony/css-selector": "^7.2",
- "symfony/dom-crawler": "^7.2",
- "symfony/error-handler": "^7.2",
- "symfony/lock": "^7.2",
- "symfony/var-dumper": "^7.2"
+ "symfony/browser-kit": "^7.3@beta",
+ "symfony/css-selector": "^7.3@beta",
+ "symfony/dom-crawler": "^7.3@beta",
+ "symfony/error-handler": "^7.3@beta",
+ "symfony/lock": "^7.3@beta",
+ "symfony/var-dumper": "^7.3@beta"
},
"replace": {
"symfony/polyfill-php72": "*",
diff --git a/composer.lock b/composer.lock
index f9484b94396e..4c80fc74acd2 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": "dcd79681afda0e50ae02b69ee396b5d3",
+ "content-hash": "e359f7a4bb5e2c4520dd885d47b22963",
"packages": [
{
"name": "asm89/stack-cors",
@@ -497,7 +497,7 @@
"dist": {
"type": "path",
"url": "core",
- "reference": "3fe3a026102b1fb6c77111c00ec61a34a42f3645"
+ "reference": "3806cfdbb344a5c7ed6e8b78534454547ccd882a"
},
"require": {
"asm89/stack-cors": "^2.3",
@@ -539,7 +539,8 @@
"symfony/http-kernel": "^7.3@beta",
"symfony/mailer": "^7.3@beta",
"symfony/mime": "^7.3@beta",
- "symfony/polyfill-iconv": "^1.26",
+ "symfony/polyfill-iconv": "^1.32",
+ "symfony/polyfill-php84": "^1.32",
"symfony/process": "^7.3@beta",
"symfony/psr-http-message-bridge": "^7.3@beta",
"symfony/routing": "^7.3@beta",
@@ -2321,16 +2322,16 @@
},
{
"name": "symfony/error-handler",
- "version": "v7.2.5",
+ "version": "v7.3.0-BETA1",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
- "reference": "102be5e6a8e4f4f3eb3149bcbfa33a80d1ee374b"
+ "reference": "47a96276149f049ba944cbd470f4d17bf42914e3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/error-handler/zipball/102be5e6a8e4f4f3eb3149bcbfa33a80d1ee374b",
- "reference": "102be5e6a8e4f4f3eb3149bcbfa33a80d1ee374b",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/47a96276149f049ba944cbd470f4d17bf42914e3",
+ "reference": "47a96276149f049ba944cbd470f4d17bf42914e3",
"shasum": ""
},
"require": {
@@ -2343,9 +2344,11 @@
"symfony/http-kernel": "<6.4"
},
"require-dev": {
+ "symfony/console": "^6.4|^7.0",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/http-kernel": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.0"
+ "symfony/serializer": "^6.4|^7.0",
+ "symfony/webpack-encore-bundle": "^1.0|^2.0"
},
"bin": [
"Resources/bin/patch-type-declarations"
@@ -2376,7 +2379,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/error-handler/tree/v7.2.5"
+ "source": "https://github.com/symfony/error-handler/tree/v7.3.0-BETA1"
},
"funding": [
{
@@ -2392,7 +2395,7 @@
"type": "tidelift"
}
],
- "time": "2025-03-03T07:12:39+00:00"
+ "time": "2025-03-17T19:44:19+00:00"
},
{
"name": "symfony/event-dispatcher",
@@ -3520,6 +3523,82 @@
"time": "2024-12-23T08:48:59+00:00"
},
{
+ "name": "symfony/polyfill-php84",
+ "version": "v1.32.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php84.git",
+ "reference": "000df7860439609837bbe28670b0be15783b7fbf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf",
+ "reference": "000df7860439609837bbe28670b0be15783b7fbf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php84\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-02-20T12:04:08+00:00"
+ },
+ {
"name": "symfony/process",
"version": "v7.3.0-BETA1",
"source": {
@@ -3927,16 +4006,16 @@
},
{
"name": "symfony/string",
- "version": "v7.2.6",
+ "version": "v7.3.0-BETA1",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "a214fe7d62bd4df2a76447c67c6b26e1d5e74931"
+ "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/a214fe7d62bd4df2a76447c67c6b26e1d5e74931",
- "reference": "a214fe7d62bd4df2a76447c67c6b26e1d5e74931",
+ "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125",
+ "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125",
"shasum": ""
},
"require": {
@@ -3994,7 +4073,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v7.2.6"
+ "source": "https://github.com/symfony/string/tree/v7.3.0-BETA1"
},
"funding": [
{
@@ -4010,7 +4089,7 @@
"type": "tidelift"
}
],
- "time": "2025-04-20T20:18:16+00:00"
+ "time": "2025-04-20T20:19:01+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -4189,20 +4268,21 @@
},
{
"name": "symfony/var-dumper",
- "version": "v7.2.6",
+ "version": "v7.3.0-BETA1",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "9c46038cd4ed68952166cf7001b54eb539184ccb"
+ "reference": "5be5bdd07600c270083d821a4b20697a47526311"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9c46038cd4ed68952166cf7001b54eb539184ccb",
- "reference": "9c46038cd4ed68952166cf7001b54eb539184ccb",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/5be5bdd07600c270083d821a4b20697a47526311",
+ "reference": "5be5bdd07600c270083d821a4b20697a47526311",
"shasum": ""
},
"require": {
"php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
@@ -4252,7 +4332,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v7.2.6"
+ "source": "https://github.com/symfony/var-dumper/tree/v7.3.0-BETA1"
},
"funding": [
{
@@ -4268,24 +4348,25 @@
"type": "tidelift"
}
],
- "time": "2025-04-09T08:14:01+00:00"
+ "time": "2025-04-09T08:14:14+00:00"
},
{
"name": "symfony/var-exporter",
- "version": "v7.2.6",
+ "version": "v7.3.0-BETA1",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-exporter.git",
- "reference": "422b8de94c738830a1e071f59ad14d67417d7007"
+ "reference": "6d25a2377310c85f0400797e4f07c303df00bd74"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-exporter/zipball/422b8de94c738830a1e071f59ad14d67417d7007",
- "reference": "422b8de94c738830a1e071f59ad14d67417d7007",
+ "url": "https://api.github.com/repos/symfony/var-exporter/zipball/6d25a2377310c85f0400797e4f07c303df00bd74",
+ "reference": "6d25a2377310c85f0400797e4f07c303df00bd74",
"shasum": ""
},
"require": {
- "php": ">=8.2"
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3"
},
"require-dev": {
"symfony/property-access": "^6.4|^7.0",
@@ -4328,7 +4409,7 @@
"serialize"
],
"support": {
- "source": "https://github.com/symfony/var-exporter/tree/v7.2.6"
+ "source": "https://github.com/symfony/var-exporter/tree/v7.3.0-BETA1"
},
"funding": [
{
@@ -4344,7 +4425,7 @@
"type": "tidelift"
}
],
- "time": "2025-05-02T08:36:00+00:00"
+ "time": "2025-05-02T08:36:13+00:00"
},
{
"name": "symfony/yaml",
@@ -9369,16 +9450,16 @@
},
{
"name": "symfony/browser-kit",
- "version": "v7.2.4",
+ "version": "v7.3.0-BETA1",
"source": {
"type": "git",
"url": "https://github.com/symfony/browser-kit.git",
- "reference": "8ce0ee23857d87d5be493abba2d52d1f9e49da61"
+ "reference": "5384291845e74fd7d54f3d925c4a86ce12336593"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/browser-kit/zipball/8ce0ee23857d87d5be493abba2d52d1f9e49da61",
- "reference": "8ce0ee23857d87d5be493abba2d52d1f9e49da61",
+ "url": "https://api.github.com/repos/symfony/browser-kit/zipball/5384291845e74fd7d54f3d925c4a86ce12336593",
+ "reference": "5384291845e74fd7d54f3d925c4a86ce12336593",
"shasum": ""
},
"require": {
@@ -9417,7 +9498,7 @@
"description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/browser-kit/tree/v7.2.4"
+ "source": "https://github.com/symfony/browser-kit/tree/v7.3.0-BETA1"
},
"funding": [
{
@@ -9433,11 +9514,11 @@
"type": "tidelift"
}
],
- "time": "2025-02-14T14:27:24+00:00"
+ "time": "2025-03-05T10:15:41+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v7.2.0",
+ "version": "v7.3.0-BETA1",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
@@ -9482,7 +9563,7 @@
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/css-selector/tree/v7.2.0"
+ "source": "https://github.com/symfony/css-selector/tree/v7.3.0-BETA1"
},
"funding": [
{
@@ -9502,16 +9583,16 @@
},
{
"name": "symfony/dom-crawler",
- "version": "v7.2.4",
+ "version": "v7.3.0-BETA1",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
- "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7"
+ "reference": "0fabbc3d6a9c473b716a93fc8e7a537adb396166"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7",
- "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/0fabbc3d6a9c473b716a93fc8e7a537adb396166",
+ "reference": "0fabbc3d6a9c473b716a93fc8e7a537adb396166",
"shasum": ""
},
"require": {
@@ -9549,7 +9630,7 @@
"description": "Eases DOM navigation for HTML and XML documents",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/dom-crawler/tree/v7.2.4"
+ "source": "https://github.com/symfony/dom-crawler/tree/v7.3.0-BETA1"
},
"funding": [
{
@@ -9565,20 +9646,20 @@
"type": "tidelift"
}
],
- "time": "2025-02-17T15:53:07+00:00"
+ "time": "2025-03-05T10:15:41+00:00"
},
{
"name": "symfony/lock",
- "version": "v7.2.6",
+ "version": "v7.3.0-BETA1",
"source": {
"type": "git",
"url": "https://github.com/symfony/lock.git",
- "reference": "69599a1d602a6c66fc69cdf733839480d01a06be"
+ "reference": "5bef45fb874b0454a616ac8091447a7982a438cf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/lock/zipball/69599a1d602a6c66fc69cdf733839480d01a06be",
- "reference": "69599a1d602a6c66fc69cdf733839480d01a06be",
+ "url": "https://api.github.com/repos/symfony/lock/zipball/5bef45fb874b0454a616ac8091447a7982a438cf",
+ "reference": "5bef45fb874b0454a616ac8091447a7982a438cf",
"shasum": ""
},
"require": {
@@ -9627,7 +9708,7 @@
"semaphore"
],
"support": {
- "source": "https://github.com/symfony/lock/tree/v7.2.6"
+ "source": "https://github.com/symfony/lock/tree/v7.3.0-BETA1"
},
"funding": [
{
@@ -9643,7 +9724,7 @@
"type": "tidelift"
}
],
- "time": "2025-04-17T22:02:25+00:00"
+ "time": "2025-04-20T20:19:01+00:00"
},
{
"name": "tbachert/spi",
@@ -9858,12 +9939,18 @@
"drupal/core": 20,
"drupal/core-project-message": 20,
"drupal/core-recipe-unpack": 20,
- "drupal/core-vendor-hardening": 20
+ "drupal/core-vendor-hardening": 20,
+ "symfony/browser-kit": 10,
+ "symfony/css-selector": 10,
+ "symfony/dom-crawler": 10,
+ "symfony/error-handler": 10,
+ "symfony/lock": 10,
+ "symfony/var-dumper": 10
},
"prefer-stable": true,
"prefer-lowest": false,
- "platform": [],
- "platform-dev": [],
+ "platform": {},
+ "platform-dev": {},
"platform-overrides": {
"php": "8.3.0"
},
diff --git a/composer/Metapackage/CoreRecommended/composer.json b/composer/Metapackage/CoreRecommended/composer.json
index bc11c39d9b80..48e38f50d342 100644
--- a/composer/Metapackage/CoreRecommended/composer.json
+++ b/composer/Metapackage/CoreRecommended/composer.json
@@ -36,7 +36,7 @@
"symfony/console": "~v7.3.0-BETA1",
"symfony/dependency-injection": "~v7.3.0-BETA1",
"symfony/deprecation-contracts": "~v3.5.1",
- "symfony/error-handler": "~v7.2.5",
+ "symfony/error-handler": "~v7.3.0-BETA1",
"symfony/event-dispatcher": "~v7.3.0-BETA1",
"symfony/event-dispatcher-contracts": "~v3.5.1",
"symfony/filesystem": "~v7.3.0-BETA1",
@@ -51,16 +51,17 @@
"symfony/polyfill-intl-idn": "~v1.32.0",
"symfony/polyfill-intl-normalizer": "~v1.32.0",
"symfony/polyfill-mbstring": "~v1.32.0",
+ "symfony/polyfill-php84": "~v1.32.0",
"symfony/process": "~v7.3.0-BETA1",
"symfony/psr-http-message-bridge": "~v7.3.0-BETA1",
"symfony/routing": "~v7.3.0-BETA1",
"symfony/serializer": "~v7.3.0-BETA1",
"symfony/service-contracts": "~v3.5.1",
- "symfony/string": "~v7.2.6",
+ "symfony/string": "~v7.3.0-BETA1",
"symfony/translation-contracts": "~v3.5.1",
"symfony/validator": "~v7.3.0-BETA1",
- "symfony/var-dumper": "~v7.2.6",
- "symfony/var-exporter": "~v7.2.6",
+ "symfony/var-dumper": "~v7.3.0-BETA1",
+ "symfony/var-exporter": "~v7.3.0-BETA1",
"symfony/yaml": "~v7.3.0-BETA1",
"twig/twig": "~v3.21.1"
}
diff --git a/composer/Metapackage/DevDependencies/composer.json b/composer/Metapackage/DevDependencies/composer.json
index 674d8e9f7fbe..402af5480da4 100644
--- a/composer/Metapackage/DevDependencies/composer.json
+++ b/composer/Metapackage/DevDependencies/composer.json
@@ -26,11 +26,11 @@
"phpstan/phpstan": "^1.12.4 || ^2.1.14",
"phpstan/phpstan-phpunit": "^1.3.16 || ^2.0.6",
"phpunit/phpunit": "^10.5.19 || ^11.5.3",
- "symfony/browser-kit": "^7.2",
- "symfony/css-selector": "^7.2",
- "symfony/dom-crawler": "^7.2",
- "symfony/error-handler": "^7.2",
- "symfony/lock": "^7.2",
- "symfony/var-dumper": "^7.2"
+ "symfony/browser-kit": "^7.3@beta",
+ "symfony/css-selector": "^7.3@beta",
+ "symfony/dom-crawler": "^7.3@beta",
+ "symfony/error-handler": "^7.3@beta",
+ "symfony/lock": "^7.3@beta",
+ "symfony/var-dumper": "^7.3@beta"
}
}
diff --git a/composer/Metapackage/PinnedDevDependencies/composer.json b/composer/Metapackage/PinnedDevDependencies/composer.json
index 5518fa7cbafd..992139dea514 100644
--- a/composer/Metapackage/PinnedDevDependencies/composer.json
+++ b/composer/Metapackage/PinnedDevDependencies/composer.json
@@ -83,10 +83,10 @@
"sirbrillig/phpcs-variable-analysis": "v2.12.0",
"slevomat/coding-standard": "8.18.0",
"squizlabs/php_codesniffer": "3.12.2",
- "symfony/browser-kit": "v7.2.4",
- "symfony/css-selector": "v7.2.0",
- "symfony/dom-crawler": "v7.2.4",
- "symfony/lock": "v7.2.6",
+ "symfony/browser-kit": "v7.3.0-BETA1",
+ "symfony/css-selector": "v7.3.0-BETA1",
+ "symfony/dom-crawler": "v7.3.0-BETA1",
+ "symfony/lock": "v7.3.0-BETA1",
"tbachert/spi": "v1.0.3",
"theseer/tokenizer": "1.2.3",
"webflo/drupal-finder": "1.3.1",
diff --git a/core/.phpstan-baseline.php b/core/.phpstan-baseline.php
index 551b1b4c9f18..6d5e0db73d98 100644
--- a/core/.phpstan-baseline.php
+++ b/core/.phpstan-baseline.php
@@ -15836,12 +15836,6 @@ $ignoreErrors[] = [
'path' => __DIR__ . '/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php',
];
$ignoreErrors[] = [
- 'message' => '#^Variable \\$unrestricted_tab_count might not be defined\\.$#',
- 'identifier' => 'variable.undefined',
- 'count' => 1,
- 'path' => __DIR__ . '/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php',
-];
-$ignoreErrors[] = [
'message' => '#^Method Drupal\\\\datetime\\\\DateTimeComputed\\:\\:setValue\\(\\) has no return type specified\\.$#',
'identifier' => 'missingType.return',
'count' => 1,
diff --git a/core/composer.json b/core/composer.json
index 59bca714e5b9..af9d9ca3162d 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -32,7 +32,8 @@
"symfony/serializer": "^7.3@beta",
"symfony/validator": "^7.3@beta",
"symfony/process": "^7.3@beta",
- "symfony/polyfill-iconv": "^1.26",
+ "symfony/polyfill-iconv": "^1.32",
+ "symfony/polyfill-php84": "^1.32",
"symfony/yaml": "^7.3@beta",
"revolt/event-loop": "^1.0",
"twig/twig": "^3.21.0",
diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php
index 63514b60eff8..9b6dc8765017 100644
--- a/core/lib/Drupal.php
+++ b/core/lib/Drupal.php
@@ -76,7 +76,7 @@ class Drupal {
/**
* The current system version.
*/
- const VERSION = '11.2-dev';
+ const VERSION = '11.3-dev';
/**
* Core API compatibility.
diff --git a/core/modules/contextual/contextual.libraries.yml b/core/modules/contextual/contextual.libraries.yml
index bfc1c996c98f..6798d02d9070 100644
--- a/core/modules/contextual/contextual.libraries.yml
+++ b/core/modules/contextual/contextual.libraries.yml
@@ -1,16 +1,9 @@
drupal.contextual-links:
version: VERSION
js:
+ js/contextualModelView.js: {}
# Ensure to run before contextual/drupal.context-toolbar.
- # Core.
js/contextual.js: { weight: -2 }
- # Models.
- js/models/StateModel.js: { weight: -2 }
- # Views.
- js/views/AuralView.js: { weight: -2 }
- js/views/KeyboardView.js: { weight: -2 }
- js/views/RegionView.js: { weight: -2 }
- js/views/VisualView.js: { weight: -2 }
css:
component:
css/contextual.module.css: {}
@@ -22,28 +15,21 @@ drupal.contextual-links:
- core/drupal
- core/drupal.ajax
- core/drupalSettings
- # @todo Remove this in https://www.drupal.org/project/drupal/issues/3203920
- - core/internal.backbone
- core/once
- core/drupal.touchevents-test
drupal.contextual-toolbar:
version: VERSION
js:
+ js/toolbar/contextualToolbarModelView.js: {}
js/contextual.toolbar.js: {}
- # Models.
- js/toolbar/models/StateModel.js: {}
- # Views.
- js/toolbar/views/AuralView.js: {}
- js/toolbar/views/VisualView.js: {}
css:
component:
css/contextual.toolbar.css: {}
dependencies:
- core/jquery
+ - contextual/drupal.contextual-links
- core/drupal
- # @todo Remove this in https://www.drupal.org/project/drupal/issues/3203920
- - core/internal.backbone
- core/once
- core/drupal.tabbingmanager
- core/drupal.announce
diff --git a/core/modules/contextual/css/contextual.theme.css b/core/modules/contextual/css/contextual.theme.css
index 06a6728be396..55a83d5ca12a 100644
--- a/core/modules/contextual/css/contextual.theme.css
+++ b/core/modules/contextual/css/contextual.theme.css
@@ -17,6 +17,10 @@
left: 0;
}
+.contextual.open {
+ z-index: 501;
+}
+
/**
* Contextual region.
*/
diff --git a/core/modules/contextual/js/contextual.js b/core/modules/contextual/js/contextual.js
index 87ccaa52dffe..f1008eabe07b 100644
--- a/core/modules/contextual/js/contextual.js
+++ b/core/modules/contextual/js/contextual.js
@@ -3,7 +3,7 @@
* Attaches behaviors for the Contextual module.
*/
-(function ($, Drupal, drupalSettings, _, Backbone, JSON, storage) {
+(function ($, Drupal, drupalSettings, JSON, storage) {
const options = $.extend(
drupalSettings.contextual,
// Merge strings on top of drupalSettings so that they are not mutable.
@@ -14,22 +14,19 @@
},
},
);
-
// Clear the cached contextual links whenever the current user's set of
// permissions changes.
const cachedPermissionsHash = storage.getItem(
'Drupal.contextual.permissionsHash',
);
- const permissionsHash = drupalSettings.user.permissionsHash;
+ const { permissionsHash } = drupalSettings.user;
if (cachedPermissionsHash !== permissionsHash) {
if (typeof permissionsHash === 'string') {
- _.chain(storage)
- .keys()
- .each((key) => {
- if (key.startsWith('Drupal.contextual.')) {
- storage.removeItem(key);
- }
- });
+ Object.keys(storage).forEach((key) => {
+ if (key.startsWith('Drupal.contextual.')) {
+ storage.removeItem(key);
+ }
+ });
}
storage.setItem('Drupal.contextual.permissionsHash', permissionsHash);
}
@@ -87,7 +84,7 @@
*/
function initContextual($contextual, html) {
const $region = $contextual.closest('.contextual-region');
- const contextual = Drupal.contextual;
+ const { contextual } = Drupal;
$contextual
// Update the placeholder to contain its rendered contextual links.
@@ -107,46 +104,18 @@
const glue = url.includes('?') ? '&' : '?';
this.setAttribute('href', url + glue + destination);
});
-
let title = '';
const $regionHeading = $region.find('h2');
if ($regionHeading.length) {
title = $regionHeading[0].textContent.trim();
}
- // Create a model and the appropriate views.
- const model = new contextual.StateModel({
- title,
- });
- const viewOptions = $.extend({ el: $contextual, model }, options);
- contextual.views.push({
- visual: new contextual.VisualView(viewOptions),
- aural: new contextual.AuralView(viewOptions),
- keyboard: new contextual.KeyboardView(viewOptions),
- });
- contextual.regionViews.push(
- new contextual.RegionView($.extend({ el: $region, model }, options)),
- );
-
- // Add the model to the collection. This must happen after the views have
- // been associated with it, otherwise collection change event handlers can't
- // trigger the model change event handler in its views.
- contextual.collection.add(model);
-
- // Let other JavaScript react to the adding of a new contextual link.
- $(document).trigger(
- 'drupalContextualLinkAdded',
- Drupal.deprecatedProperty({
- target: {
- $el: $contextual,
- $region,
- model,
- },
- deprecatedProperty: 'model',
- message:
- 'The model property is deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no replacement.',
- }),
+ options.title = title;
+ const contextualModelView = new Drupal.contextual.ContextualModelView(
+ $contextual,
+ $region,
+ options,
);
-
+ contextual.instances.push(contextualModelView);
// Fix visual collisions between contextual link triggers.
adjustIfNestedAndOverlapping($contextual);
}
@@ -192,7 +161,7 @@
// Initialize after the current execution cycle, to make the AJAX
// request for retrieving the uncached contextual links as soon as
// possible, but also to ensure that other Drupal behaviors have had
- // the chance to set up an event listener on the Backbone collection
+ // the chance to set up an event listener on the collection
// Drupal.contextual.collection.
window.setTimeout(() => {
initContextual(
@@ -217,7 +186,7 @@
data: { 'ids[]': uncachedIDs, 'tokens[]': uncachedTokens },
dataType: 'json',
success(results) {
- _.each(results, (html, contextualID) => {
+ Object.entries(results).forEach(([contextualID, html]) => {
// Store the metadata.
storage.setItem(`Drupal.contextual.${contextualID}`, html);
// If the rendered contextual links are empty, then the current
@@ -274,21 +243,23 @@
* replacement.
*/
regionViews: [],
+ instances: new Proxy([], {
+ set: function set(obj, prop, value) {
+ obj[prop] = value;
+ window.dispatchEvent(new Event('contextual-instances-added'));
+ return true;
+ },
+ deleteProperty(target, prop) {
+ if (prop in target) {
+ delete target[prop];
+ window.dispatchEvent(new Event('contextual-instances-removed'));
+ }
+ },
+ }),
+ ContextualModelView: {},
};
/**
- * A Backbone.Collection of {@link Drupal.contextual.StateModel} instances.
- *
- * @type {Backbone.Collection}
- *
- * @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
- * replacement.
- */
- Drupal.contextual.collection = new Backbone.Collection([], {
- model: Drupal.contextual.StateModel,
- });
-
- /**
* A trigger is an interactive element often bound to a click handler.
*
* @return {string}
@@ -311,12 +282,4 @@
$(document).on('drupalContextualLinkAdded', (event, data) => {
Drupal.ajax.bindAjaxLinks(data.$el[0]);
});
-})(
- jQuery,
- Drupal,
- drupalSettings,
- _,
- Backbone,
- window.JSON,
- window.sessionStorage,
-);
+})(jQuery, Drupal, drupalSettings, window.JSON, window.sessionStorage);
diff --git a/core/modules/contextual/js/contextual.toolbar.js b/core/modules/contextual/js/contextual.toolbar.js
index 8fc206cc2c3b..c94d0df414c9 100644
--- a/core/modules/contextual/js/contextual.toolbar.js
+++ b/core/modules/contextual/js/contextual.toolbar.js
@@ -3,7 +3,7 @@
* Attaches behaviors for the Contextual module's edit toolbar tab.
*/
-(function ($, Drupal, Backbone) {
+(function ($, Drupal) {
const strings = {
tabbingReleased: Drupal.t(
'Tabbing is no longer constrained by the Contextual module.',
@@ -21,33 +21,19 @@
* A contextual links DOM element as rendered by the server.
*/
function initContextualToolbar(context) {
- if (!Drupal.contextual || !Drupal.contextual.collection) {
+ if (!Drupal.contextual || !Drupal.contextual.instances) {
return;
}
- const contextualToolbar = Drupal.contextualToolbar;
- contextualToolbar.model = new contextualToolbar.StateModel(
- {
- // Checks whether localStorage indicates we should start in edit mode
- // rather than view mode.
- // @see Drupal.contextualToolbar.VisualView.persist
- isViewing:
- document.querySelector('body .contextual-region') === null ||
- localStorage.getItem('Drupal.contextualToolbar.isViewing') !==
- 'false',
- },
- {
- contextualCollection: Drupal.contextual.collection,
- },
- );
+ const { contextualToolbar } = Drupal;
const viewOptions = {
el: $('.toolbar .toolbar-bar .contextual-toolbar-tab'),
- model: contextualToolbar.model,
strings,
};
- new contextualToolbar.VisualView(viewOptions);
- new contextualToolbar.AuralView(viewOptions);
+ contextualToolbar.model = new Drupal.contextual.ContextualToolbarModelView(
+ viewOptions,
+ );
}
/**
@@ -75,13 +61,10 @@
*/
Drupal.contextualToolbar = {
/**
- * The {@link Drupal.contextualToolbar.StateModel} instance.
- *
- * @type {?Drupal.contextualToolbar.StateModel}
+ * The {@link Drupal.contextual.ContextualToolbarModelView} instance.
*
- * @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is
- * no replacement.
+ * @type {?Drupal.contextual.ContextualToolbarModelView}
*/
model: null,
};
-})(jQuery, Drupal, Backbone);
+})(jQuery, Drupal);
diff --git a/core/modules/contextual/js/contextualModelView.js b/core/modules/contextual/js/contextualModelView.js
new file mode 100644
index 000000000000..4488045e2236
--- /dev/null
+++ b/core/modules/contextual/js/contextualModelView.js
@@ -0,0 +1,254 @@
+(($, Drupal) => {
+ /**
+ * Models the state of a contextual link's trigger, list & region.
+ */
+ Drupal.contextual.ContextualModelView = class {
+ constructor($contextual, $region, options) {
+ this.title = options.title || '';
+ this.regionIsHovered = false;
+ this._hasFocus = false;
+ this._isOpen = false;
+ this._isLocked = false;
+ this.strings = options.strings;
+ this.timer = NaN;
+ this.modelId = btoa(Math.random()).substring(0, 12);
+ this.$region = $region;
+ this.$contextual = $contextual;
+
+ if (!document.body.classList.contains('touchevents')) {
+ this.$region.on({
+ mouseenter: () => {
+ this.regionIsHovered = true;
+ },
+ mouseleave: () => {
+ this.close().blur();
+ this.regionIsHovered = false;
+ },
+ 'mouseleave mouseenter': () => this.render(),
+ });
+ this.$contextual.on('mouseenter', () => {
+ this.focus();
+ this.render();
+ });
+ }
+
+ this.$contextual.on(
+ {
+ click: () => {
+ this.toggleOpen();
+ },
+ touchend: () => {
+ Drupal.contextual.ContextualModelView.touchEndToClick();
+ },
+ focus: () => {
+ this.focus();
+ },
+ blur: () => {
+ this.blur();
+ },
+ 'click blur touchend focus': () => this.render(),
+ },
+ '.trigger',
+ );
+
+ this.$contextual.on(
+ {
+ click: () => {
+ this.close().blur();
+ },
+ touchend: (event) => {
+ Drupal.contextual.ContextualModelView.touchEndToClick(event);
+ },
+ focus: () => {
+ this.focus();
+ },
+ blur: () => {
+ this.waitCloseThenBlur();
+ },
+ 'click blur touchend focus': () => this.render(),
+ },
+ '.contextual-links a',
+ );
+
+ this.render();
+
+ // Let other JavaScript react to the adding of a new contextual link.
+ $(document).trigger('drupalContextualLinkAdded', {
+ $el: $contextual,
+ $region,
+ model: this,
+ });
+ }
+
+ /**
+ * Updates the rendered representation of the current contextual links.
+ */
+ render() {
+ const { isOpen } = this;
+ const isVisible = this.isLocked || this.regionIsHovered || isOpen;
+ this.$region.toggleClass('focus', this.hasFocus);
+ this.$contextual
+ .toggleClass('open', isOpen)
+ // Update the visibility of the trigger.
+ .find('.trigger')
+ .toggleClass('visually-hidden', !isVisible);
+
+ this.$contextual.find('.contextual-links').prop('hidden', !isOpen);
+ const trigger = this.$contextual.find('.trigger').get(0);
+ trigger.textContent = Drupal.t('@action @title configuration options', {
+ '@action': !isOpen ? this.strings.open : this.strings.close,
+ '@title': this.title,
+ });
+ trigger.setAttribute('aria-pressed', isOpen);
+ }
+
+ /**
+ * Prevents delay and simulated mouse events.
+ *
+ * @param {jQuery.Event} event the touch end event.
+ */
+ static touchEndToClick(event) {
+ event.preventDefault();
+ event.target.click();
+ }
+
+ /**
+ * Set up a timeout to allow a user to tab between the trigger and the
+ * contextual links without the menu dismissing.
+ */
+ waitCloseThenBlur() {
+ this.timer = window.setTimeout(() => {
+ this.isOpen = false;
+ this.hasFocus = false;
+ this.render();
+ }, 150);
+ }
+
+ /**
+ * Opens or closes the contextual link.
+ *
+ * If it is opened, then also give focus.
+ *
+ * @return {Drupal.contextual.ContextualModelView}
+ * The current contextual model view.
+ */
+ toggleOpen() {
+ const newIsOpen = !this.isOpen;
+ this.isOpen = newIsOpen;
+ if (newIsOpen) {
+ this.focus();
+ }
+ return this;
+ }
+
+ /**
+ * Gives focus to this contextual link.
+ *
+ * Also closes + removes focus from every other contextual link.
+ *
+ * @return {Drupal.contextual.ContextualModelView}
+ * The current contextual model view.
+ */
+ focus() {
+ const { modelId } = this;
+ Drupal.contextual.instances.forEach((model) => {
+ if (model.modelId !== modelId) {
+ model.close().blur();
+ }
+ });
+ window.clearTimeout(this.timer);
+ this.hasFocus = true;
+ return this;
+ }
+
+ /**
+ * Removes focus from this contextual link, unless it is open.
+ *
+ * @return {Drupal.contextual.ContextualModelView}
+ * The current contextual model view.
+ */
+ blur() {
+ if (!this.isOpen) {
+ this.hasFocus = false;
+ }
+ return this;
+ }
+
+ /**
+ * Closes this contextual link.
+ *
+ * Does not call blur() because we want to allow a contextual link to have
+ * focus, yet be closed for example when hovering.
+ *
+ * @return {Drupal.contextual.ContextualModelView}
+ * The current contextual model view.
+ */
+ close() {
+ this.isOpen = false;
+ return this;
+ }
+
+ /**
+ * Gets the current focus state.
+ *
+ * @return {boolean} the focus state.
+ */
+ get hasFocus() {
+ return this._hasFocus;
+ }
+
+ /**
+ * Sets the current focus state.
+ *
+ * @param {boolean} value - new focus state
+ */
+ set hasFocus(value) {
+ this._hasFocus = value;
+ this.$region.toggleClass('focus', this._hasFocus);
+ }
+
+ /**
+ * Gets the current open state.
+ *
+ * @return {boolean} the open state.
+ */
+ get isOpen() {
+ return this._isOpen;
+ }
+
+ /**
+ * Sets the current open state.
+ *
+ * @param {boolean} value - new open state
+ */
+ set isOpen(value) {
+ this._isOpen = value;
+ // Nested contextual region handling: hide any nested contextual triggers.
+ this.$region
+ .closest('.contextual-region')
+ .find('.contextual .trigger:not(:first)')
+ .toggle(!this.isOpen);
+ }
+
+ /**
+ * Gets the current locked state.
+ *
+ * @return {boolean} the locked state.
+ */
+ get isLocked() {
+ return this._isLocked;
+ }
+
+ /**
+ * Sets the current locked state.
+ *
+ * @param {boolean} value - new locked state
+ */
+ set isLocked(value) {
+ if (value !== this._isLocked) {
+ this._isLocked = value;
+ this.render();
+ }
+ }
+ };
+})(jQuery, Drupal);
diff --git a/core/modules/contextual/js/models/StateModel.js b/core/modules/contextual/js/models/StateModel.js
deleted file mode 100644
index 622f897917f5..000000000000
--- a/core/modules/contextual/js/models/StateModel.js
+++ /dev/null
@@ -1,130 +0,0 @@
-/**
- * @file
- * A Backbone Model for the state of a contextual link's trigger, list & region.
- */
-
-(function (Drupal, Backbone) {
- /**
- * Models the state of a contextual link's trigger, list & region.
- *
- * @constructor
- *
- * @augments Backbone.Model
- *
- * @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
- * replacement.
- */
- Drupal.contextual.StateModel = Backbone.Model.extend(
- /** @lends Drupal.contextual.StateModel# */ {
- /**
- * @type {object}
- *
- * @prop {string} title
- * @prop {boolean} regionIsHovered
- * @prop {boolean} hasFocus
- * @prop {boolean} isOpen
- * @prop {boolean} isLocked
- */
- defaults: /** @lends Drupal.contextual.StateModel# */ {
- /**
- * The title of the entity to which these contextual links apply.
- *
- * @type {string}
- */
- title: '',
-
- /**
- * Represents if the contextual region is being hovered.
- *
- * @type {boolean}
- */
- regionIsHovered: false,
-
- /**
- * Represents if the contextual trigger or options have focus.
- *
- * @type {boolean}
- */
- hasFocus: false,
-
- /**
- * Represents if the contextual options for an entity are available to
- * be selected (i.e. whether the list of options is visible).
- *
- * @type {boolean}
- */
- isOpen: false,
-
- /**
- * When the model is locked, the trigger remains active.
- *
- * @type {boolean}
- */
- isLocked: false,
- },
-
- /**
- * Opens or closes the contextual link.
- *
- * If it is opened, then also give focus.
- *
- * @return {Drupal.contextual.StateModel}
- * The current contextual state model.
- */
- toggleOpen() {
- const newIsOpen = !this.get('isOpen');
- this.set('isOpen', newIsOpen);
- if (newIsOpen) {
- this.focus();
- }
- return this;
- },
-
- /**
- * Closes this contextual link.
- *
- * Does not call blur() because we want to allow a contextual link to have
- * focus, yet be closed for example when hovering.
- *
- * @return {Drupal.contextual.StateModel}
- * The current contextual state model.
- */
- close() {
- this.set('isOpen', false);
- return this;
- },
-
- /**
- * Gives focus to this contextual link.
- *
- * Also closes + removes focus from every other contextual link.
- *
- * @return {Drupal.contextual.StateModel}
- * The current contextual state model.
- */
- focus() {
- this.set('hasFocus', true);
- const cid = this.cid;
- this.collection.each((model) => {
- if (model.cid !== cid) {
- model.close().blur();
- }
- });
- return this;
- },
-
- /**
- * Removes focus from this contextual link, unless it is open.
- *
- * @return {Drupal.contextual.StateModel}
- * The current contextual state model.
- */
- blur() {
- if (!this.get('isOpen')) {
- this.set('hasFocus', false);
- }
- return this;
- },
- },
- );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/js/toolbar/contextualToolbarModelView.js b/core/modules/contextual/js/toolbar/contextualToolbarModelView.js
new file mode 100644
index 000000000000..6c6db5fe70cd
--- /dev/null
+++ b/core/modules/contextual/js/toolbar/contextualToolbarModelView.js
@@ -0,0 +1,175 @@
+(($, Drupal) => {
+ Drupal.contextual.ContextualToolbarModelView = class {
+ constructor(options) {
+ this.strings = options.strings;
+ this.isVisible = false;
+ this._contextualCount = Drupal.contextual.instances.count;
+ this.tabbingContext = null;
+ this._isViewing =
+ localStorage.getItem('Drupal.contextualToolbar.isViewing') !== 'false';
+ this.$el = options.el;
+
+ window.addEventListener('contextual-instances-added', () =>
+ this.lockNewContextualLinks(),
+ );
+ window.addEventListener('contextual-instances-removed', () => {
+ this.contextualCount = Drupal.contextual.instances.count;
+ });
+
+ this.$el.on({
+ click: () => {
+ this.isViewing = !this.isViewing;
+ },
+ touchend: (event) => {
+ event.preventDefault();
+ event.target.click();
+ },
+ 'click touchend': () => this.render(),
+ });
+
+ $(document).on('keyup', (event) => this.onKeypress(event));
+ this.manageTabbing(true);
+ this.render();
+ }
+
+ /**
+ * Responds to esc and tab key press events.
+ *
+ * @param {jQuery.Event} event
+ * The keypress event.
+ */
+ onKeypress(event) {
+ // The first tab key press is tracked so that an announcement about
+ // tabbing constraints can be raised if edit mode is enabled when the page
+ // is loaded.
+ if (!this.announcedOnce && event.keyCode === 9 && !this.isViewing) {
+ this.announceTabbingConstraint();
+ // Set announce to true so that this conditional block won't run again.
+ this.announcedOnce = true;
+ }
+ // Respond to the ESC key. Exit out of edit mode.
+ if (event.keyCode === 27) {
+ this.isViewing = true;
+ }
+ }
+
+ /**
+ * Updates the rendered representation of the current toolbar model view.
+ */
+ render() {
+ this.$el[0].classList.toggle('hidden', this.isVisible);
+ const button = this.$el[0].querySelector('button');
+ button.classList.toggle('is-active', !this.isViewing);
+ button.setAttribute('aria-pressed', !this.isViewing);
+ this.contextualCount = Drupal.contextual.instances.count;
+ }
+
+ /**
+ * Automatically updates visibility of the view/edit mode toggle.
+ */
+ updateVisibility() {
+ this.isVisible = this.get('contextualCount') > 0;
+ }
+
+ /**
+ * Lock newly added contextual links if edit mode is enabled.
+ */
+ lockNewContextualLinks() {
+ Drupal.contextual.instances.forEach((model) => {
+ model.isLocked = !this.isViewing;
+ });
+ this.contextualCount = Drupal.contextual.instances.count;
+ }
+
+ /**
+ * Limits tabbing to the contextual links and edit mode toolbar tab.
+ *
+ * @param {boolean} init - true to initialize tabbing.
+ */
+ manageTabbing(init = false) {
+ let { tabbingContext } = this;
+ // Always release an existing tabbing context.
+ if (tabbingContext && !init) {
+ // Only announce release when the context was active.
+ if (tabbingContext.active) {
+ Drupal.announce(this.strings.tabbingReleased);
+ }
+ tabbingContext.release();
+ this.tabbingContext = null;
+ }
+ // Create a new tabbing context when edit mode is enabled.
+ if (!this.isViewing) {
+ tabbingContext = Drupal.tabbingManager.constrain(
+ $('.contextual-toolbar-tab, .contextual'),
+ );
+ this.tabbingContext = tabbingContext;
+ this.announceTabbingConstraint();
+ this.announcedOnce = true;
+ }
+ }
+
+ /**
+ * Announces the current tabbing constraint.
+ */
+ announceTabbingConstraint() {
+ const { strings } = this;
+ Drupal.announce(
+ Drupal.formatString(strings.tabbingConstrained, {
+ '@contextualsCount': Drupal.formatPlural(
+ Drupal.contextual.instances.length,
+ '@count contextual link',
+ '@count contextual links',
+ ),
+ }) + strings.pressEsc,
+ );
+ }
+
+ /**
+ * Gets the current viewing state.
+ *
+ * @return {boolean} the viewing state.
+ */
+ get isViewing() {
+ return this._isViewing;
+ }
+
+ /**
+ * Sets the current viewing state.
+ *
+ * @param {boolean} value - new viewing state
+ */
+ set isViewing(value) {
+ this._isViewing = value;
+ localStorage[!value ? 'setItem' : 'removeItem'](
+ 'Drupal.contextualToolbar.isViewing',
+ 'false',
+ );
+
+ Drupal.contextual.instances.forEach((model) => {
+ model.isLocked = !this.isViewing;
+ });
+ this.manageTabbing();
+ }
+
+ /**
+ * Gets the current contextual links count.
+ *
+ * @return {integer} the current contextual links count.
+ */
+ get contextualCount() {
+ return this._contextualCount;
+ }
+
+ /**
+ * Sets the current contextual links count.
+ *
+ * @param {integer} value - new contextual links count.
+ */
+ set contextualCount(value) {
+ if (value !== this._contextualCount) {
+ this._contextualCount = value;
+ this.updateVisibility();
+ }
+ }
+ };
+})(jQuery, Drupal);
diff --git a/core/modules/contextual/js/toolbar/models/StateModel.js b/core/modules/contextual/js/toolbar/models/StateModel.js
deleted file mode 100644
index 88f66193f9f3..000000000000
--- a/core/modules/contextual/js/toolbar/models/StateModel.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * @file
- * A Backbone Model for the state of Contextual module's edit toolbar tab.
- */
-
-(function (Drupal, Backbone) {
- /**
- * @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
- * replacement.
- */
- Drupal.contextualToolbar.StateModel = Backbone.Model.extend(
- /** @lends Drupal.contextualToolbar.StateModel# */ {
- /**
- * @type {object}
- *
- * @prop {boolean} isViewing
- * @prop {boolean} isVisible
- * @prop {number} contextualCount
- * @prop {Drupal~TabbingContext} tabbingContext
- */
- defaults: /** @lends Drupal.contextualToolbar.StateModel# */ {
- /**
- * Indicates whether the toggle is currently in "view" or "edit" mode.
- *
- * @type {boolean}
- */
- isViewing: true,
-
- /**
- * Indicates whether the toggle should be visible or hidden. Automatically
- * calculated, depends on contextualCount.
- *
- * @type {boolean}
- */
- isVisible: false,
-
- /**
- * Tracks how many contextual links exist on the page.
- *
- * @type {number}
- */
- contextualCount: 0,
-
- /**
- * A TabbingContext object as returned by {@link Drupal~TabbingManager}:
- * the set of tabbable elements when edit mode is enabled.
- *
- * @type {?Drupal~TabbingContext}
- */
- tabbingContext: null,
- },
-
- /**
- * Models the state of the edit mode toggle.
- *
- * @constructs
- *
- * @augments Backbone.Model
- *
- * @param {object} attrs
- * Attributes for the backbone model.
- * @param {object} options
- * An object with the following option:
- * @param {Backbone.collection} options.contextualCollection
- * The collection of {@link Drupal.contextual.StateModel} models that
- * represent the contextual links on the page.
- */
- initialize(attrs, options) {
- // Respond to new/removed contextual links.
- this.listenTo(
- options.contextualCollection,
- 'reset remove add',
- this.countContextualLinks,
- );
- this.listenTo(
- options.contextualCollection,
- 'add',
- this.lockNewContextualLinks,
- );
-
- // Automatically determine visibility.
- this.listenTo(this, 'change:contextualCount', this.updateVisibility);
-
- // Whenever edit mode is toggled, lock all contextual links.
- this.listenTo(this, 'change:isViewing', (model, isViewing) => {
- options.contextualCollection.each((contextualModel) => {
- contextualModel.set('isLocked', !isViewing);
- });
- });
- },
-
- /**
- * Tracks the number of contextual link models in the collection.
- *
- * @param {Drupal.contextual.StateModel} contextualModel
- * The contextual links model that was added or removed.
- * @param {Backbone.Collection} contextualCollection
- * The collection of contextual link models.
- */
- countContextualLinks(contextualModel, contextualCollection) {
- this.set('contextualCount', contextualCollection.length);
- },
-
- /**
- * Lock newly added contextual links if edit mode is enabled.
- *
- * @param {Drupal.contextual.StateModel} contextualModel
- * The contextual links model that was added.
- * @param {Backbone.Collection} [contextualCollection]
- * The collection of contextual link models.
- */
- lockNewContextualLinks(contextualModel, contextualCollection) {
- if (!this.get('isViewing')) {
- contextualModel.set('isLocked', true);
- }
- },
-
- /**
- * Automatically updates visibility of the view/edit mode toggle.
- */
- updateVisibility() {
- this.set('isVisible', this.get('contextualCount') > 0);
- },
- },
- );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/js/toolbar/views/AuralView.js b/core/modules/contextual/js/toolbar/views/AuralView.js
deleted file mode 100644
index 2bcf9cdcca0f..000000000000
--- a/core/modules/contextual/js/toolbar/views/AuralView.js
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- * @file
- * A Backbone View that provides the aural view of the edit mode toggle.
- */
-
-(function ($, Drupal, Backbone, _) {
- /**
- * @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
- * replacement.
- */
- Drupal.contextualToolbar.AuralView = Backbone.View.extend(
- /** @lends Drupal.contextualToolbar.AuralView# */ {
- /**
- * Tracks whether the tabbing constraint announcement has been read once.
- *
- * @type {boolean}
- */
- announcedOnce: false,
-
- /**
- * Renders the aural view of the edit mode toggle (screen reader support).
- *
- * @constructs
- *
- * @augments Backbone.View
- *
- * @param {object} options
- * Options for the view.
- */
- initialize(options) {
- this.options = options;
-
- this.listenTo(this.model, 'change', this.render);
- this.listenTo(this.model, 'change:isViewing', this.manageTabbing);
-
- $(document).on('keyup', _.bind(this.onKeypress, this));
- this.manageTabbing();
- },
-
- /**
- * {@inheritdoc}
- *
- * @return {Drupal.contextualToolbar.AuralView}
- * The current contextual toolbar aural view.
- */
- render() {
- // Render the state.
- this.$el
- .find('button')
- .attr('aria-pressed', !this.model.get('isViewing'));
-
- return this;
- },
-
- /**
- * Limits tabbing to the contextual links and edit mode toolbar tab.
- */
- manageTabbing() {
- let tabbingContext = this.model.get('tabbingContext');
- // Always release an existing tabbing context.
- if (tabbingContext) {
- // Only announce release when the context was active.
- if (tabbingContext.active) {
- Drupal.announce(this.options.strings.tabbingReleased);
- }
- tabbingContext.release();
- }
- // Create a new tabbing context when edit mode is enabled.
- if (!this.model.get('isViewing')) {
- tabbingContext = Drupal.tabbingManager.constrain(
- $('.contextual-toolbar-tab, .contextual'),
- );
- this.model.set('tabbingContext', tabbingContext);
- this.announceTabbingConstraint();
- this.announcedOnce = true;
- }
- },
-
- /**
- * Announces the current tabbing constraint.
- */
- announceTabbingConstraint() {
- const strings = this.options.strings;
- Drupal.announce(
- Drupal.formatString(strings.tabbingConstrained, {
- '@contextualsCount': Drupal.formatPlural(
- Drupal.contextual.collection.length,
- '@count contextual link',
- '@count contextual links',
- ),
- }),
- );
- Drupal.announce(strings.pressEsc);
- },
-
- /**
- * Responds to esc and tab key press events.
- *
- * @param {jQuery.Event} event
- * The keypress event.
- */
- onKeypress(event) {
- // The first tab key press is tracked so that an announcement about
- // tabbing constraints can be raised if edit mode is enabled when the page
- // is loaded.
- if (
- !this.announcedOnce &&
- event.keyCode === 9 &&
- !this.model.get('isViewing')
- ) {
- this.announceTabbingConstraint();
- // Set announce to true so that this conditional block won't run again.
- this.announcedOnce = true;
- }
- // Respond to the ESC key. Exit out of edit mode.
- if (event.keyCode === 27) {
- this.model.set('isViewing', true);
- }
- },
- },
- );
-})(jQuery, Drupal, Backbone, _);
diff --git a/core/modules/contextual/js/toolbar/views/VisualView.js b/core/modules/contextual/js/toolbar/views/VisualView.js
deleted file mode 100644
index 10d8dff2deaa..000000000000
--- a/core/modules/contextual/js/toolbar/views/VisualView.js
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * @file
- * A Backbone View that provides the visual view of the edit mode toggle.
- */
-
-(function (Drupal, Backbone) {
- /**
- * @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
- * replacement.
- */
- Drupal.contextualToolbar.VisualView = Backbone.View.extend(
- /** @lends Drupal.contextualToolbar.VisualView# */ {
- /**
- * Events for the Backbone view.
- *
- * @return {object}
- * A mapping of events to be used in the view.
- */
- events() {
- // Prevents delay and simulated mouse events.
- const touchEndToClick = function (event) {
- event.preventDefault();
- event.target.click();
- };
-
- return {
- click() {
- this.model.set('isViewing', !this.model.get('isViewing'));
- },
- touchend: touchEndToClick,
- };
- },
-
- /**
- * Renders the visual view of the edit mode toggle.
- *
- * Listens to mouse & touch and handles edit mode toggle interactions.
- *
- * @constructs
- *
- * @augments Backbone.View
- */
- initialize() {
- this.listenTo(this.model, 'change', this.render);
- this.listenTo(this.model, 'change:isViewing', this.persist);
- },
-
- /**
- * {@inheritdoc}
- *
- * @return {Drupal.contextualToolbar.VisualView}
- * The current contextual toolbar visual view.
- */
- render() {
- // Render the visibility.
- this.$el.toggleClass('hidden', !this.model.get('isVisible'));
- // Render the state.
- this.$el
- .find('button')
- .toggleClass('is-active', !this.model.get('isViewing'));
-
- return this;
- },
-
- /**
- * Model change handler; persists the isViewing value to localStorage.
- *
- * `isViewing === true` is the default, so only stores in localStorage when
- * it's not the default value (i.e. false).
- *
- * @param {Drupal.contextualToolbar.StateModel} model
- * A {@link Drupal.contextualToolbar.StateModel} model.
- * @param {boolean} isViewing
- * The value of the isViewing attribute in the model.
- */
- persist(model, isViewing) {
- if (!isViewing) {
- localStorage.setItem('Drupal.contextualToolbar.isViewing', 'false');
- } else {
- localStorage.removeItem('Drupal.contextualToolbar.isViewing');
- }
- },
- },
- );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/js/views/AuralView.js b/core/modules/contextual/js/views/AuralView.js
deleted file mode 100644
index 62287c1bf118..000000000000
--- a/core/modules/contextual/js/views/AuralView.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * @file
- * A Backbone View that provides the aural view of a contextual link.
- */
-
-(function (Drupal, Backbone) {
- /**
- * @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
- * replacement.
- */
- Drupal.contextual.AuralView = Backbone.View.extend(
- /** @lends Drupal.contextual.AuralView# */ {
- /**
- * Renders the aural view of a contextual link (i.e. screen reader support).
- *
- * @constructs
- *
- * @augments Backbone.View
- *
- * @param {object} options
- * Options for the view.
- */
- initialize(options) {
- this.options = options;
-
- this.listenTo(this.model, 'change', this.render);
-
- // Initial render.
- this.render();
- },
-
- /**
- * {@inheritdoc}
- */
- render() {
- const isOpen = this.model.get('isOpen');
-
- // Set the hidden property of the links.
- this.$el.find('.contextual-links').prop('hidden', !isOpen);
-
- // Update the view of the trigger.
- const $trigger = this.$el.find('.trigger');
- $trigger
- .each((index, element) => {
- element.textContent = Drupal.t(
- '@action @title configuration options',
- {
- '@action': !isOpen
- ? this.options.strings.open
- : this.options.strings.close,
- '@title': this.model.get('title'),
- },
- );
- })
- .attr('aria-pressed', isOpen);
- },
- },
- );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/js/views/KeyboardView.js b/core/modules/contextual/js/views/KeyboardView.js
deleted file mode 100644
index 2a3d144bea07..000000000000
--- a/core/modules/contextual/js/views/KeyboardView.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * @file
- * A Backbone View that provides keyboard interaction for a contextual link.
- */
-
-(function (Drupal, Backbone) {
- /**
- * @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
- * replacement.
- */
- Drupal.contextual.KeyboardView = Backbone.View.extend(
- /** @lends Drupal.contextual.KeyboardView# */ {
- /**
- * @type {object}
- */
- events: {
- 'focus .trigger': 'focus',
- 'focus .contextual-links a': 'focus',
- 'blur .trigger': function () {
- this.model.blur();
- },
- 'blur .contextual-links a': function () {
- // Set up a timeout to allow a user to tab between the trigger and the
- // contextual links without the menu dismissing.
- const that = this;
- this.timer = window.setTimeout(() => {
- that.model.close().blur();
- }, 150);
- },
- },
-
- /**
- * Provides keyboard interaction for a contextual link.
- *
- * @constructs
- *
- * @augments Backbone.View
- */
- initialize() {
- /**
- * The timer is used to create a delay before dismissing the contextual
- * links on blur. This is only necessary when keyboard users tab into
- * contextual links without edit mode (i.e. without TabbingManager).
- * That means that if we decide to disable tabbing of contextual links
- * without edit mode, all this timer logic can go away.
- *
- * @type {NaN|number}
- */
- this.timer = NaN;
- },
-
- /**
- * Sets focus on the model; Clears the timer that dismisses the links.
- */
- focus() {
- // Clear the timeout that might have been set by blurring a link.
- window.clearTimeout(this.timer);
- this.model.focus();
- },
- },
- );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/js/views/RegionView.js b/core/modules/contextual/js/views/RegionView.js
deleted file mode 100644
index 349428301d81..000000000000
--- a/core/modules/contextual/js/views/RegionView.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * @file
- * A Backbone View that renders the visual view of a contextual region element.
- */
-
-(function (Drupal, Backbone) {
- /**
- * @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
- * replacement.
- */
- Drupal.contextual.RegionView = Backbone.View.extend(
- /** @lends Drupal.contextual.RegionView# */ {
- /**
- * Events for the Backbone view.
- *
- * @return {object}
- * A mapping of events to be used in the view.
- */
- events() {
- // Used for tracking the presence of touch events. When true, the
- // mousemove and mouseenter event handlers are effectively disabled.
- // This is used instead of preventDefault() on touchstart as some
- // touchstart events are not cancelable.
- let touchStart = false;
- return {
- touchstart() {
- // Set to true so the mouseenter and mouseleave events that follow
- // know to not execute any hover related logic.
- touchStart = true;
- },
- mouseenter() {
- if (!touchStart) {
- this.model.set('regionIsHovered', true);
- }
- },
- mouseleave() {
- if (!touchStart) {
- this.model.close().blur().set('regionIsHovered', false);
- }
- },
- mousemove() {
- // Because there are scenarios where there are both touchscreens
- // and pointer devices, the touchStart flag should be set back to
- // false after mouseenter and mouseleave complete. It will be set to
- // true if another touchstart event occurs.
- touchStart = false;
- },
- };
- },
-
- /**
- * Renders the visual view of a contextual region element.
- *
- * @constructs
- *
- * @augments Backbone.View
- */
- initialize() {
- this.listenTo(this.model, 'change:hasFocus', this.render);
- },
-
- /**
- * {@inheritdoc}
- *
- * @return {Drupal.contextual.RegionView}
- * The current contextual region view.
- */
- render() {
- this.$el.toggleClass('focus', this.model.get('hasFocus'));
-
- return this;
- },
- },
- );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/js/views/VisualView.js b/core/modules/contextual/js/views/VisualView.js
deleted file mode 100644
index fcd932b1faf4..000000000000
--- a/core/modules/contextual/js/views/VisualView.js
+++ /dev/null
@@ -1,109 +0,0 @@
-/**
- * @file
- * A Backbone View that provides the visual view of a contextual link.
- */
-
-(function (Drupal, Backbone) {
- /**
- * @deprecated in drupal:9.4.0 and is removed from drupal:12.0.0. There is no
- * replacement.
- */
- Drupal.contextual.VisualView = Backbone.View.extend(
- /** @lends Drupal.contextual.VisualView# */ {
- /**
- * Events for the Backbone view.
- *
- * @return {object}
- * A mapping of events to be used in the view.
- */
- events() {
- // Prevents delay and simulated mouse events.
- const touchEndToClick = function (event) {
- event.preventDefault();
- event.target.click();
- };
-
- // Used for tracking the presence of touch events. When true, the
- // mousemove and mouseenter event handlers are effectively disabled.
- // This is used instead of preventDefault() on touchstart as some
- // touchstart events are not cancelable.
- let touchStart = false;
-
- return {
- touchstart() {
- // Set to true so the mouseenter events that follows knows to not
- // execute any hover related logic.
- touchStart = true;
- },
- mouseenter() {
- // We only want mouse hover events on non-touch.
- if (!touchStart) {
- this.model.focus();
- }
- },
- mousemove() {
- // Because there are scenarios where there are both touchscreens
- // and pointer devices, the touchStart flag should be set back to
- // false after mouseenter and mouseleave complete. It will be set to
- // true if another touchstart event occurs.
- touchStart = false;
- },
- 'click .trigger': function () {
- this.model.toggleOpen();
- },
- 'touchend .trigger': touchEndToClick,
- 'click .contextual-links a': function () {
- this.model.close().blur();
- },
- 'touchend .contextual-links a': touchEndToClick,
- };
- },
-
- /**
- * Renders the visual view of a contextual link. Listens to mouse & touch.
- *
- * @constructs
- *
- * @augments Backbone.View
- */
- initialize() {
- this.listenTo(this.model, 'change', this.render);
- },
-
- /**
- * {@inheritdoc}
- *
- * @return {Drupal.contextual.VisualView}
- * The current contextual visual view.
- */
- render() {
- const isOpen = this.model.get('isOpen');
- // The trigger should be visible when:
- // - the mouse hovered over the region,
- // - the trigger is locked,
- // - and for as long as the contextual menu is open.
- const isVisible =
- this.model.get('isLocked') ||
- this.model.get('regionIsHovered') ||
- isOpen;
-
- this.$el
- // The open state determines if the links are visible.
- .toggleClass('open', isOpen)
- // Update the visibility of the trigger.
- .find('.trigger')
- .toggleClass('visually-hidden', !isVisible);
-
- // Nested contextual region handling: hide any nested contextual triggers.
- if ('isOpen' in this.model.changed) {
- this.$el
- .closest('.contextual-region')
- .find('.contextual .trigger:not(:first)')
- .toggle(!isOpen);
- }
-
- return this;
- },
- },
- );
-})(Drupal, Backbone);
diff --git a/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php b/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php
index 75e56b5f76b2..1d4fa243c492 100644
--- a/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php
+++ b/core/modules/contextual/tests/src/FunctionalJavascript/EditModeTest.php
@@ -73,47 +73,40 @@ class EditModeTest extends WebDriverTestBase {
$page = $this->getSession()->getPage();
// Get the page twice to ensure edit mode remains enabled after a new page
// request.
- for ($page_get_count = 0; $page_get_count < 2; $page_get_count++) {
- $this->drupalGet('user');
- $expected_restricted_tab_count = 1 + count($page->findAll('css', '[data-contextual-id]'));
-
- // After the page loaded we need to additionally wait until the settings
- // tray Ajax activity is done.
- if ($page_get_count === 0) {
- $web_assert->assertWaitOnAjaxRequest();
- }
-
- if ($page_get_count == 0) {
- $unrestricted_tab_count = $this->getTabbableElementsCount();
- $this->assertGreaterThan($expected_restricted_tab_count, $unrestricted_tab_count);
-
- // Enable edit mode.
- // After the first page load the page will be in edit mode when loaded.
- $this->pressToolbarEditButton();
- }
-
- $this->assertAnnounceEditMode();
- $this->assertSame($expected_restricted_tab_count, $this->getTabbableElementsCount());
-
- // Disable edit mode.
- $this->pressToolbarEditButton();
- $this->assertAnnounceLeaveEditMode();
- $this->assertSame($unrestricted_tab_count, $this->getTabbableElementsCount());
- // Enable edit mode again.
- $this->pressToolbarEditButton();
- // Finally assert that the 'edit mode enabled' announcement is still
- // correct after toggling the edit mode at least once.
- $this->assertAnnounceEditMode();
- $this->assertSame($expected_restricted_tab_count, $this->getTabbableElementsCount());
-
- // Test while Edit Mode is enabled it doesn't interfere with pages with
- // no contextual links.
- $this->drupalGet('admin/structure/block');
- $web_assert->elementContains('css', 'h1.page-title', 'Block layout');
- $this->assertEquals(0, count($page->findAll('css', '[data-contextual-id]')));
- $this->assertGreaterThan(0, $this->getTabbableElementsCount());
- }
-
+ $this->drupalGet('user');
+ $expected_restricted_tab_count = 1 + count($page->findAll('css', '[data-contextual-id]'));
+
+ // After the page loaded we need to additionally wait until the settings
+ // tray Ajax activity is done.
+ $web_assert->assertWaitOnAjaxRequest();
+
+ $unrestricted_tab_count = $this->getTabbableElementsCount();
+ $this->assertGreaterThan($expected_restricted_tab_count, $unrestricted_tab_count);
+
+ // Enable edit mode.
+ // After the first page load the page will be in edit mode when loaded.
+ $this->pressToolbarEditButton();
+
+ $this->assertAnnounceEditMode();
+ $this->assertSame($expected_restricted_tab_count, $this->getTabbableElementsCount());
+
+ // Disable edit mode.
+ $this->pressToolbarEditButton();
+ $this->assertAnnounceLeaveEditMode();
+ $this->assertSame($unrestricted_tab_count, $this->getTabbableElementsCount());
+ // Enable edit mode again.
+ $this->pressToolbarEditButton();
+ // Finally assert that the 'edit mode enabled' announcement is still
+ // correct after toggling the edit mode at least once.
+ $this->assertAnnounceEditMode();
+ $this->assertSame($expected_restricted_tab_count, $this->getTabbableElementsCount());
+
+ // Test while Edit Mode is enabled it doesn't interfere with pages with
+ // no contextual links.
+ $this->drupalGet('admin/structure/block');
+ $web_assert->elementContains('css', 'h1.page-title', 'Block layout');
+ $this->assertEquals(0, count($page->findAll('css', '[data-contextual-id]')));
+ $this->assertGreaterThan(0, $this->getTabbableElementsCount());
}
/**
diff --git a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
index 2371bef31aac..ae9ae566f8dd 100644
--- a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
+++ b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php
@@ -91,7 +91,7 @@ class PerformanceTest extends PerformanceTestBase {
'CacheTagInvalidationCount' => 0,
'CacheTagLookupQueryCount' => 14,
'ScriptCount' => 3,
- 'ScriptBytes' => 213500,
+ 'ScriptBytes' => 167569,
'StylesheetCount' => 2,
'StylesheetBytes' => 46000,
];
diff --git a/core/modules/package_manager/tests/src/Build/TemplateProjectTestBase.php b/core/modules/package_manager/tests/src/Build/TemplateProjectTestBase.php
index 16cd486ad750..18b63b8376bd 100644
--- a/core/modules/package_manager/tests/src/Build/TemplateProjectTestBase.php
+++ b/core/modules/package_manager/tests/src/Build/TemplateProjectTestBase.php
@@ -441,6 +441,8 @@ END;
$requirements['symfony/polyfill-php81'],
$requirements['symfony/polyfill-php82'],
$requirements['symfony/polyfill-php83'],
+ // Needed for PHP 8.4 features while PHP 8.3 is the minimum.
+ $requirements['symfony/polyfill-php84'],
);
// If this package requires any Drupal core packages, ensure it allows
// any version.
diff --git a/core/modules/toolbar/js/views/ToolbarVisualView.js b/core/modules/toolbar/js/views/ToolbarVisualView.js
index 89f472f0eafa..00bd236973f6 100644
--- a/core/modules/toolbar/js/views/ToolbarVisualView.js
+++ b/core/modules/toolbar/js/views/ToolbarVisualView.js
@@ -210,7 +210,7 @@
// Deactivate the previous tab.
$(this.model.previous('activeTab'))
.removeClass('is-active')
- .prop('aria-pressed', false);
+ .attr('aria-pressed', false);
// Deactivate the previous tray.
$(this.model.previous('activeTray')).removeClass('is-active');
@@ -222,7 +222,7 @@
$tab
.addClass('is-active')
// Mark the tab as pressed.
- .prop('aria-pressed', true);
+ .attr('aria-pressed', true);
const name = $tab.attr('data-toolbar-tray');
// Store the active tab name or remove the setting.
const id = $tab.get(0).id;
diff --git a/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarIntegrationTest.php b/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarIntegrationTest.php
index c315f9f6ebb0..dcf0ff6d79c3 100644
--- a/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarIntegrationTest.php
+++ b/core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarIntegrationTest.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Drupal\Tests\toolbar\FunctionalJavascript;
+use Behat\Mink\Element\NodeElement;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
@@ -43,12 +44,22 @@ class ToolbarIntegrationTest extends WebDriverTestBase {
$page = $this->getSession()->getPage();
// Test that it is possible to toggle the toolbar tray.
- $content = $page->findLink('Content');
- $this->assertTrue($content->isVisible(), 'Toolbar tray is open by default.');
- $page->clickLink('Manage');
- $this->assertFalse($content->isVisible(), 'Toolbar tray is closed after clicking the "Manage" link.');
- $page->clickLink('Manage');
- $this->assertTrue($content->isVisible(), 'Toolbar tray is visible again after clicking the "Manage" button a second time.');
+ $content_link = $page->findLink('Content');
+ $manage_link = $page->find('css', '#toolbar-item-administration');
+
+ // Start with open tray.
+ $this->waitAndAssertAriaPressedState($manage_link, TRUE);
+ $this->assertTrue($content_link->isVisible(), 'Toolbar tray is open by default.');
+
+ // Click to close.
+ $manage_link->click();
+ $this->waitAndAssertAriaPressedState($manage_link, FALSE);
+ $this->assertFalse($content_link->isVisible(), 'Toolbar tray is closed after clicking the "Manage" link.');
+
+ // Click to open.
+ $manage_link->click();
+ $this->waitAndAssertAriaPressedState($manage_link, TRUE);
+ $this->assertTrue($content_link->isVisible(), 'Toolbar tray is visible again after clicking the "Manage" button a second time.');
// Test toggling the toolbar tray between horizontal and vertical.
$tray = $page->findById('toolbar-item-administration-tray');
@@ -87,4 +98,33 @@ class ToolbarIntegrationTest extends WebDriverTestBase {
$this->assertFalse($button->isVisible(), 'Orientation toggle from other tray is not visible');
}
+ /**
+ * Asserts that an element's `aria-pressed` attribute matches expected state.
+ *
+ * Uses `waitFor()` to pause until either the condition is met or the timeout
+ * of `1` second has passed.
+ *
+ * @param \Behat\Mink\Element\NodeElement $element
+ * The element to be tested.
+ * @param bool $expected
+ * The expected value of `aria-pressed`, as a boolean.
+ *
+ * @throws ExpectationFailedException
+ */
+ private function waitAndAssertAriaPressedState(NodeElement $element, bool $expected): void {
+ $this->assertTrue(
+ $this
+ ->getSession()
+ ->getPage()
+ ->waitFor(1, function () use ($element, $expected): bool {
+ // Get boolean representation of `aria-pressed`.
+ // TRUE if `aria-pressed="true"`, FALSE otherwise.
+ $actual = $element->getAttribute('aria-pressed') == 'true';
+
+ // Exit `waitFor()` when $actual == $expected.
+ return $actual == $expected;
+ })
+ );
+ }
+
}
diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/AssetAggregationAcrossPagesTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/AssetAggregationAcrossPagesTest.php
index 0b56bdd18c39..c7358357f3c5 100644
--- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/AssetAggregationAcrossPagesTest.php
+++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/AssetAggregationAcrossPagesTest.php
@@ -69,7 +69,7 @@ class AssetAggregationAcrossPagesTest extends PerformanceTestBase {
}, 'umamiFrontAndRecipePagesEditor');
$expected = [
'ScriptCount' => 5,
- 'ScriptBytes' => 338200,
+ 'ScriptBytes' => 335637,
'StylesheetCount' => 5,
'StylesheetBytes' => 205700,
];
diff --git a/core/themes/claro/css/classy/components/tablesort.css b/core/themes/claro/css/classy/components/tablesort.css
index 44e5349404d0..f2a3c4ad60a3 100644
--- a/core/themes/claro/css/classy/components/tablesort.css
+++ b/core/themes/claro/css/classy/components/tablesort.css
@@ -6,6 +6,3 @@
th.is-active img {
display: inline;
}
-td.is-active {
- background-color: #ddd;
-}
diff --git a/core/themes/claro/css/components/dialog.css b/core/themes/claro/css/components/dialog.css
index e1d0b18f3bca..ecaf17d2daae 100644
--- a/core/themes/claro/css/components/dialog.css
+++ b/core/themes/claro/css/components/dialog.css
@@ -96,7 +96,8 @@
@media (forced-colors: active) {
.ui-dialog .ui-dialog-titlebar .ui-dialog-titlebar-close .ui-icon.ui-icon-closethick {
- background: url("data:image/svg+xml,%3csvg width='12' height='12' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M11 1.318l-10 10M11 11.318l-10-10' stroke='buttonText' stroke-width='1.5'/%3e%3c/svg%3e") no-repeat 50%;
+ background: buttontext;
+ mask: url("data:image/svg+xml,%3csvg width='12' height='12' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M11 1.318l-10 10M11 11.318l-10-10' stroke='%23D3D4D9' stroke-width='1.5'/%3e%3c/svg%3e") no-repeat 50%;
}
}
diff --git a/core/themes/claro/css/components/dialog.pcss.css b/core/themes/claro/css/components/dialog.pcss.css
index ffec289fbc43..a965222cf7c3 100644
--- a/core/themes/claro/css/components/dialog.pcss.css
+++ b/core/themes/claro/css/components/dialog.pcss.css
@@ -86,7 +86,8 @@
background: url(../../images/icons/d3d4d9/ex.svg) no-repeat 50%;
@media (forced-colors: active) {
- background: url(../../images/icons/buttonText/ex.svg) no-repeat 50%;
+ background: buttontext;
+ mask: url(../../images/icons/d3d4d9/ex.svg) no-repeat 50%;
}
}
}
diff --git a/core/themes/claro/images/icons/buttonText/ex.svg b/core/themes/claro/images/icons/buttonText/ex.svg
deleted file mode 100644
index 635ac1c6b382..000000000000
--- a/core/themes/claro/images/icons/buttonText/ex.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg width="12" height="12" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M11 1.318l-10 10M11 11.318l-10-10" stroke="buttonText" stroke-width="1.5"/></svg>
diff --git a/core/themes/olivero/olivero.libraries.yml b/core/themes/olivero/olivero.libraries.yml
index c699ebc72c8d..137a4296eb67 100644
--- a/core/themes/olivero/olivero.libraries.yml
+++ b/core/themes/olivero/olivero.libraries.yml
@@ -45,7 +45,6 @@ global-styling:
css/components/site-header.css: {}
css/components/skip-link.css: {}
css/components/pager.css: {}
- css/components/table.css: {}
css/components/text-content.css: {}
css/components/wide-content.css: {}
@@ -291,3 +290,18 @@ tags:
css:
theme:
css/components/tags.css: {}
+
+olivero.table:
+ version: VERSION
+ css:
+ component:
+ css/components/table.css: {}
+ moved_files:
+ olivero/global-styling:
+ deprecation_version: 11.2.0
+ removed_version: 12.0.0
+ deprecation_link: https://www.drupal.org/node/3517675
+ css:
+ component:
+ css/components/table.css:
+ base: css/components/table.css
diff --git a/core/themes/olivero/olivero.theme b/core/themes/olivero/olivero.theme
index d10ee7d155cd..b2f3bff26841 100644
--- a/core/themes/olivero/olivero.theme
+++ b/core/themes/olivero/olivero.theme
@@ -617,6 +617,15 @@ function olivero_preprocess_table(&$variables): void {
}
}
}
+
+ $variables['#attached']['library'][] = 'olivero/olivero.table';
+}
+
+/**
+ * Implements hook_preprocess_HOOK() for views-view-table templates.
+ */
+function olivero_preprocess_views_view_table(&$variables): void {
+ $variables['#attached']['library'][] = 'olivero/olivero.table';
}
/**