diff options
61 files changed, 714 insertions, 404 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 93640185196..c6cb7502b17 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -556,7 +556,7 @@ default: <<: [ *default-job-settings-lint, *default-phpunit-job-settings ] variables: TESTSUITE: PHPUnit-Unit - KUBERNETES_CPU_REQUEST: "8" + KUBERNETES_CPU_REQUEST: "4" CONCURRENCY: 24 _TARGET_PHP: "8.3-ubuntu" @@ -564,7 +564,7 @@ default: <<: [ *default-job-settings-lint, *default-phpunit-job-settings ] variables: TESTSUITE: PHPUnit-Unit - KUBERNETES_CPU_REQUEST: "8" + KUBERNETES_CPU_REQUEST: "4" CONCURRENCY: 24 _TARGET_PHP: "8.4-ubuntu" @@ -573,7 +573,7 @@ default: allow_failure: true variables: TESTSUITE: PHPUnit-Unit - KUBERNETES_CPU_REQUEST: "8" + KUBERNETES_CPU_REQUEST: "4" CONCURRENCY: 24 _TARGET_PHP: "8.5-ubuntu" @@ -636,6 +636,7 @@ default: - "**/config/schema/*.schema.yml" # Modules may alter config schema using hook_config_schema_info_alter(). - "**/*.module" + - "**/src/Hook/*.php" - when: manual allow_failure: true artifacts: diff --git a/core/.phpstan-baseline.php b/core/.phpstan-baseline.php index 21b7ee78a04..f413f2c8a31 100644 --- a/core/.phpstan-baseline.php +++ b/core/.phpstan-baseline.php @@ -41931,18 +41931,6 @@ $ignoreErrors[] = [ 'path' => __DIR__ . '/modules/views/src/Views.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\views\\\\ViewsConfigUpdater\\:\\:create\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/views/src/ViewsConfigUpdater.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Method Drupal\\\\views\\\\ViewsConfigUpdater\\:\\:setDeprecationsEnabled\\(\\) has no return type specified\\.$#', - 'identifier' => 'missingType.return', - 'count' => 1, - 'path' => __DIR__ . '/modules/views/src/ViewsConfigUpdater.php', -]; -$ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\views\\\\ViewsData\\:\\:cacheSet\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', 'count' => 1, diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 4803c6599b8..80851b7113f 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -695,6 +695,21 @@ drupal.textarea-resize: css/components/resize.module.css: component: misc/components/resize.module.css +drupal.reset-appearance: + version: VERSION + css: + component: + misc/components/reset-appearance.module.css: { weight: -10 } + moved_files: + system/base: + deprecation_version: 11.3.0 + removed_version: 12.0.0 + deprecation_link: https://www.drupal.org/node/3432346 + css: + component: + css/components/reset-appearance.module.css: + component: misc/components/reset-appearance.module.css + drupal.states: version: VERSION js: diff --git a/core/core.services.yml b/core/core.services.yml index 9f7c1a2d11c..2090ae1ceb5 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -515,12 +515,14 @@ services: keyvalue: class: Drupal\Core\KeyValueStore\KeyValueFactory arguments: ['@service_container', '%factory.keyvalue%'] + Drupal\Core\KeyValueStore\KeyValueFactoryInterface: '@keyvalue' keyvalue.database: class: Drupal\Core\KeyValueStore\KeyValueDatabaseFactory arguments: ['@serialization.phpserialize', '@database'] keyvalue.expirable: class: Drupal\Core\KeyValueStore\KeyValueExpirableFactory arguments: ['@service_container', '%factory.keyvalue.expirable%'] + Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface: '@keyvalue.expirable' keyvalue.expirable.database: class: Drupal\Core\KeyValueStore\KeyValueDatabaseExpirableFactory arguments: ['@serialization.phpserialize', '@database', '@datetime.time'] diff --git a/core/lib/Drupal/Component/Datetime/DateTimePlus.php b/core/lib/Drupal/Component/Datetime/DateTimePlus.php index 4f95d6d8b66..90d9c300854 100644 --- a/core/lib/Drupal/Component/Datetime/DateTimePlus.php +++ b/core/lib/Drupal/Component/Datetime/DateTimePlus.php @@ -53,62 +53,6 @@ class DateTimePlus { const RFC7231 = 'D, d M Y H:i:s \G\M\T'; /** - * An array of possible date parts. - * - * @var string[] - */ - protected static $dateParts = [ - 'year', - 'month', - 'day', - 'hour', - 'minute', - 'second', - ]; - - /** - * The value of the time value passed to the constructor. - * - * @var string - */ - protected $inputTimeRaw = ''; - - /** - * The prepared time, without timezone, for this date. - * - * @var string - */ - protected $inputTimeAdjusted = ''; - - /** - * The value of the timezone passed to the constructor. - * - * @var string - */ - protected $inputTimeZoneRaw = ''; - - /** - * The prepared timezone object used to construct this date. - * - * @var string - */ - protected $inputTimeZoneAdjusted = ''; - - /** - * The value of the format passed to the constructor. - * - * @var string - */ - protected $inputFormatRaw = ''; - - /** - * The prepared format, if provided. - * - * @var string - */ - protected $inputFormatAdjusted = ''; - - /** * The value of the language code passed to the constructor. * * @var string|null diff --git a/core/misc/ajax.js b/core/misc/ajax.js index a9ce2cf3fcd..d7c776a7065 100644 --- a/core/misc/ajax.js +++ b/core/misc/ajax.js @@ -1337,14 +1337,16 @@ // Parse response.data into an element collection. const parseHTML = (htmlString) => { const fragment = document.createDocumentFragment(); - // Create a temporary div element - const tempDiv = fragment.appendChild(document.createElement('div')); + // Create a temporary template element. + const template = fragment.appendChild( + document.createElement('template'), + ); - // Set the innerHTML of the div to the provided HTML string - tempDiv.innerHTML = htmlString; + // Set the innerHTML of the template to the provided HTML string. + template.innerHTML = htmlString; - // Return the contents of the temporary div - return tempDiv.childNodes; + // Return the contents of the temporary template. + return template.content.childNodes; }; let $newContent = $(parseHTML(response.data)); diff --git a/core/modules/system/css/components/reset-appearance.module.css b/core/misc/components/reset-appearance.module.css index 59741a85ce8..59741a85ce8 100644 --- a/core/modules/system/css/components/reset-appearance.module.css +++ b/core/misc/components/reset-appearance.module.css diff --git a/core/misc/htmx/htmx-assets.js b/core/misc/htmx/htmx-assets.js index bc5f00df432..9175405d047 100644 --- a/core/misc/htmx/htmx-assets.js +++ b/core/misc/htmx/htmx-assets.js @@ -166,7 +166,8 @@ // @see https://htmx.org/events/#htmx:afterSettle htmx.on('htmx:afterSettle', ({ detail }) => { requestAssetsLoaded.get(detail.xhr).then(() => { - htmx.trigger(detail.elt, 'htmx:drupal:load'); + // Some HTMX swaps put the incoming element before or after detail.elt. + htmx.trigger(detail.elt.parentNode, 'htmx:drupal:load'); // This should be automatic but don't wait for the garbage collector. requestAssetsLoaded.delete(detail.xhr); }); diff --git a/core/modules/content_translation/src/Access/ContentTranslationDeleteAccess.php b/core/modules/content_translation/src/Access/ContentTranslationDeleteAccess.php index 5f1933714bc..476eaaaf72f 100644 --- a/core/modules/content_translation/src/Access/ContentTranslationDeleteAccess.php +++ b/core/modules/content_translation/src/Access/ContentTranslationDeleteAccess.php @@ -11,7 +11,6 @@ use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\language\Entity\ContentLanguageSettings; -use Drupal\workflows\Entity\Workflow; /** * Access check for entity translation deletion. @@ -83,13 +82,9 @@ class ContentTranslationDeleteAccess implements AccessInterface { $entity_type_id = $entity->getEntityTypeId(); $result->addCacheableDependency($entity); - // Add the cache dependencies used by - // ContentTranslationManager::isPendingRevisionSupportEnabled(). - if (\Drupal::moduleHandler()->moduleExists('content_moderation')) { - foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) { - $result->addCacheableDependency($workflow); - } - } + // The information about workflows is stored in entity bundle info, depend + // on that cache tag. + $result->addCacheTags(['entity_bundles']); if (!ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle())) { return $result; } diff --git a/core/modules/content_translation/src/ContentTranslationManager.php b/core/modules/content_translation/src/ContentTranslationManager.php index 56ac55cf7e7..0718a4cbf9d 100644 --- a/core/modules/content_translation/src/ContentTranslationManager.php +++ b/core/modules/content_translation/src/ContentTranslationManager.php @@ -3,7 +3,6 @@ namespace Drupal\content_translation; use Drupal\Core\Entity\EntityInterface; -use Drupal\workflows\Entity\Workflow; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -168,24 +167,13 @@ class ContentTranslationManager implements ContentTranslationManagerInterface, B return FALSE; } - foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) { - /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */ - $plugin = $workflow->getTypePlugin(); - $entity_type_ids = array_flip($plugin->getEntityTypes()); - if (isset($entity_type_ids[$entity_type_id])) { - if (!isset($bundle_id)) { - return TRUE; - } - else { - $bundle_ids = array_flip($plugin->getBundlesForEntityType($entity_type_id)); - if (isset($bundle_ids[$bundle_id])) { - return TRUE; - } - } - } + $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); + if ($bundle_id) { + return \Drupal::service('content_moderation.moderation_information')->shouldModerateEntitiesOfBundle($entity_type, $bundle_id); + } + else { + return \Drupal::service('content_moderation.moderation_information')->canModerateEntitiesOfEntityType($entity_type); } - - return FALSE; } } diff --git a/core/modules/content_translation/src/Hook/ContentTranslationHooks.php b/core/modules/content_translation/src/Hook/ContentTranslationHooks.php index c681b07c059..9842c7085fd 100644 --- a/core/modules/content_translation/src/Hook/ContentTranslationHooks.php +++ b/core/modules/content_translation/src/Hook/ContentTranslationHooks.php @@ -10,7 +10,6 @@ use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\content_translation\ContentTranslationManager; use Drupal\content_translation\BundleTranslationSettingsInterface; use Drupal\language\ContentLanguageSettingsInterface; use Drupal\Core\Language\LanguageInterface; @@ -18,6 +17,7 @@ use Drupal\Core\Url; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Hook\Order\Order; +use Drupal\workflows\Entity\Workflow; /** * Hook implementations for content_translation. @@ -231,11 +231,27 @@ class ContentTranslationHooks { $bundle_info['translatable'] = $content_translation_manager->isEnabled($entity_type_id, $bundle); if ($bundle_info['translatable'] && $content_translation_manager instanceof BundleTranslationSettingsInterface) { $settings = $content_translation_manager->getBundleTranslationSettings($entity_type_id, $bundle); - // If pending revision support is enabled for this bundle, we need to - // hide untranslatable field widgets, otherwise changes in pending - // revisions might be overridden by changes in later default - // revisions. - $bundle_info['untranslatable_fields.default_translation_affected'] = !empty($settings['untranslatable_fields_hide']) || ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $bundle); + $bundle_info['untranslatable_fields.default_translation_affected'] = !empty($settings['untranslatable_fields_hide']); + } + } + } + + // Always hide untranslatable field widgets if pending revision support is + // enabled otherwise changes in pending + // revisions might be overridden by changes in later default revisions. + // This can't use + // Drupal\content_translation\ContentTranslationManager::isPendingRevisionSupportEnabled() + // since that depends on entity bundle information to be completely built. + if (\Drupal::moduleHandler()->moduleExists('content_moderation')) { + foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) { + /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */ + $plugin = $workflow->getTypePlugin(); + foreach ($plugin->getEntityTypes() as $entity_type_id) { + foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) { + if (isset($bundles[$entity_type_id][$bundle_id])) { + $bundles[$entity_type_id][$bundle_id]['untranslatable_fields.default_translation_affected'] = TRUE; + } + } } } } diff --git a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php index ad6c5712d71..528c874649f 100644 --- a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php +++ b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php @@ -94,7 +94,7 @@ class PerformanceTest extends PerformanceTestBase { 'ScriptCount' => 3, 'ScriptBytes' => 167569, 'StylesheetCount' => 2, - 'StylesheetBytes' => 46000, + 'StylesheetBytes' => 45450, ]; $this->assertMetrics($expected, $performance_data); diff --git a/core/modules/package_manager/package_manager.services.yml b/core/modules/package_manager/package_manager.services.yml index 059be554270..ead88254823 100644 --- a/core/modules/package_manager/package_manager.services.yml +++ b/core/modules/package_manager/package_manager.services.yml @@ -14,9 +14,6 @@ services: Drupal\package_manager\ExecutableFinder: public: false decorates: 'PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface' - Drupal\package_manager\ProcessFactory: - public: false - decorates: 'PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface' Drupal\package_manager\TranslatableStringFactory: public: false decorates: 'PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface' @@ -175,7 +172,7 @@ services: PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface: class: PhpTuf\ComposerStager\Internal\Process\Factory\ProcessFactory PhpTuf\ComposerStager\API\Process\Service\ComposerProcessRunnerInterface: - class: PhpTuf\ComposerStager\Internal\Process\Service\ComposerProcessRunner + class: Drupal\package_manager\ComposerRunner PhpTuf\ComposerStager\API\Process\Service\OutputCallbackInterface: class: PhpTuf\ComposerStager\Internal\Process\Service\OutputCallback PhpTuf\ComposerStager\API\Process\Service\ProcessInterface: diff --git a/core/modules/package_manager/src/ComposerRunner.php b/core/modules/package_manager/src/ComposerRunner.php new file mode 100644 index 00000000000..dbc029f6aa3 --- /dev/null +++ b/core/modules/package_manager/src/ComposerRunner.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\package_manager; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\File\FileSystemInterface; +use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; +use PhpTuf\ComposerStager\API\Path\Value\PathInterface; +use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; +use PhpTuf\ComposerStager\API\Process\Service\ComposerProcessRunnerInterface; +use PhpTuf\ComposerStager\API\Process\Service\OutputCallbackInterface; +use PhpTuf\ComposerStager\API\Process\Service\ProcessInterface; +use Symfony\Component\Process\PhpExecutableFinder; + +// cspell:ignore BINDIR + +/** + * Runs Composer through the current PHP interpreter. + * + * @internal + * This is an internal part of Package Manager and may be changed or removed + * at any time without warning. External code should not interact with this + * class. + */ +final class ComposerRunner implements ComposerProcessRunnerInterface { + + public function __construct( + private readonly ExecutableFinderInterface $executableFinder, + private readonly ProcessFactoryInterface $processFactory, + private readonly FileSystemInterface $fileSystem, + private readonly ConfigFactoryInterface $configFactory, + ) {} + + /** + * {@inheritdoc} + */ + public function run(array $command, ?PathInterface $cwd = NULL, array $env = [], ?OutputCallbackInterface $callback = NULL, int $timeout = ProcessInterface::DEFAULT_TIMEOUT): void { + // Run Composer through the PHP interpreter so we don't have to rely on + // PHP being in the PATH. + array_unshift($command, (new PhpExecutableFinder())->find(), $this->executableFinder->find('composer')); + + $home = $this->fileSystem->getTempDirectory(); + $home .= '/package_manager_composer_home-'; + $home .= $this->configFactory->get('system.site')->get('uuid'); + $this->fileSystem->prepareDirectory($home, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); + + $process = $this->processFactory->create($command, $cwd, $env + ['COMPOSER_HOME' => $home]); + $process->setTimeout($timeout); + $process->mustRun($callback); + } + +} diff --git a/core/modules/package_manager/src/ProcessFactory.php b/core/modules/package_manager/src/ProcessFactory.php deleted file mode 100644 index 7e92977e7ff..00000000000 --- a/core/modules/package_manager/src/ProcessFactory.php +++ /dev/null @@ -1,97 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\package_manager; - -use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\File\FileSystemInterface; -use PhpTuf\ComposerStager\API\Path\Value\PathInterface; -use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; -use PhpTuf\ComposerStager\API\Process\Service\ProcessInterface; - -// cspell:ignore BINDIR - -/** - * Defines a process factory which sets the COMPOSER_HOME environment variable. - * - * @internal - * This is an internal part of Package Manager and may be changed or removed - * at any time without warning. External code should not interact with this - * class. - */ -final class ProcessFactory implements ProcessFactoryInterface { - - public function __construct( - private readonly FileSystemInterface $fileSystem, - private readonly ConfigFactoryInterface $configFactory, - private readonly ProcessFactoryInterface $decorated, - ) {} - - /** - * {@inheritdoc} - */ - public function create(array $command, ?PathInterface $cwd = NULL, array $env = []): ProcessInterface { - $process = $this->decorated->create($command, $cwd, $env); - - $env = $process->getEnv(); - if ($command && $this->isComposerCommand($command)) { - $env['COMPOSER_HOME'] = $this->getComposerHomePath(); - } - // Ensure that the current PHP installation is the first place that will be - // searched when looking for the PHP interpreter. - $env['PATH'] = static::getPhpDirectory() . ':' . getenv('PATH'); - $process->setEnv($env); - return $process; - } - - /** - * Returns the directory which contains the PHP interpreter. - * - * @return string - * The path of the directory containing the PHP interpreter. If the server - * is running in a command-line interface, the directory portion of - * PHP_BINARY is returned; otherwise, the compile-time PHP_BINDIR is. - * - * @see php_sapi_name() - * @see https://www.php.net/manual/en/reserved.constants.php - */ - private static function getPhpDirectory(): string { - if (PHP_SAPI === 'cli' || PHP_SAPI === 'cli-server') { - return dirname(PHP_BINARY); - } - return PHP_BINDIR; - } - - /** - * Returns the path to use as the COMPOSER_HOME environment variable. - * - * @return string - * The path which should be used as COMPOSER_HOME. - */ - private function getComposerHomePath(): string { - $home_path = $this->fileSystem->getTempDirectory(); - $home_path .= '/package_manager_composer_home-'; - $home_path .= $this->configFactory->get('system.site')->get('uuid'); - $this->fileSystem->prepareDirectory($home_path, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); - - return $home_path; - } - - /** - * Determines if a command is running Composer. - * - * @param string[] $command - * The command parts. - * - * @return bool - * TRUE if the command is running Composer, FALSE otherwise. - */ - private function isComposerCommand(array $command): bool { - $executable = $command[0]; - $executable_parts = explode('/', $executable); - $file = array_pop($executable_parts); - return str_starts_with($file, 'composer'); - } - -} diff --git a/core/modules/package_manager/tests/src/Functional/ComposerRequirementTest.php b/core/modules/package_manager/tests/src/Functional/ComposerRequirementTest.php index 45046a8e2ad..15a7a2aab6b 100644 --- a/core/modules/package_manager/tests/src/Functional/ComposerRequirementTest.php +++ b/core/modules/package_manager/tests/src/Functional/ComposerRequirementTest.php @@ -47,7 +47,10 @@ class ComposerRequirementTest extends PackageManagerTestBase { $config->set('executables.composer', '/path/to/composer')->save(); $this->getSession()->reload(); $assert_session->statusCodeEquals(200); - $assert_session->pageTextContains('Composer was not found. The error message was: Failed to run process: The command "\'/path/to/composer\' \'--format=json\'" failed.'); + $assert_session->pageTextContains('Composer was not found. The error message was: '); + // Check for the part of the command string that is constant (the path to + // the PHP interpreter will vary). + $assert_session->pageTextContains("/php' '/path/to/composer' '--format=json'\" failed."); } } diff --git a/core/modules/package_manager/tests/src/Kernel/ProcessFactoryTest.php b/core/modules/package_manager/tests/src/Kernel/ProcessFactoryTest.php deleted file mode 100644 index db27b06efa8..00000000000 --- a/core/modules/package_manager/tests/src/Kernel/ProcessFactoryTest.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\Tests\package_manager\Kernel; - -use Drupal\package_manager\ProcessFactory; -use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; - -/** - * @coversDefaultClass \Drupal\package_manager\ProcessFactory - * @group auto_updates - * @internal - */ -class ProcessFactoryTest extends PackageManagerKernelTestBase { - - /** - * Tests that the process factory prepends the PHP directory to PATH. - */ - public function testPhpDirectoryPrependedToPath(): void { - $factory = $this->container->get(ProcessFactoryInterface::class); - $this->assertInstanceOf(ProcessFactory::class, $factory); - - // Ensure that the directory of the PHP interpreter can be found. - $reflector = new \ReflectionObject($factory); - $method = $reflector->getMethod('getPhpDirectory'); - $php_dir = $method->invoke(NULL); - $this->assertNotEmpty($php_dir); - - // The process factory should always put the PHP interpreter's directory - // at the beginning of the PATH environment variable. - $env = $factory->create(['whoami'])->getEnv(); - $this->assertStringStartsWith("$php_dir:", $env['PATH']); - } - -} diff --git a/core/modules/package_manager/tests/src/Kernel/ServicesTest.php b/core/modules/package_manager/tests/src/Kernel/ServicesTest.php index a1db5a517c1..9e52f9b4075 100644 --- a/core/modules/package_manager/tests/src/Kernel/ServicesTest.php +++ b/core/modules/package_manager/tests/src/Kernel/ServicesTest.php @@ -9,14 +9,12 @@ use Drupal\package_manager\ExecutableFinder; use Drupal\package_manager\LoggingBeginner; use Drupal\package_manager\LoggingCommitter; use Drupal\package_manager\LoggingStager; -use Drupal\package_manager\ProcessFactory; use Drupal\package_manager\TranslatableStringFactory; use Drupal\Tests\package_manager\Traits\AssertPreconditionsTrait; use PhpTuf\ComposerStager\API\Core\BeginnerInterface; use PhpTuf\ComposerStager\API\Core\CommitterInterface; use PhpTuf\ComposerStager\API\Core\StagerInterface; use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; -use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; use PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface; /** @@ -38,11 +36,9 @@ class ServicesTest extends KernelTestBase { * Tests that Package Manager's public services can be instantiated. */ public function testPackageManagerServices(): void { - // Ensure that any overridden Composer Stager services were overridden - // correctly. + // Ensure that certain Composer Stager services are decorated correctly. $overrides = [ ExecutableFinderInterface::class => ExecutableFinder::class, - ProcessFactoryInterface::class => ProcessFactory::class, TranslatableFactoryInterface::class => TranslatableStringFactory::class, BeginnerInterface::class => LoggingBeginner::class, StagerInterface::class => LoggingStager::class, diff --git a/core/modules/package_manager/tests/src/Unit/ComposerRunnerTest.php b/core/modules/package_manager/tests/src/Unit/ComposerRunnerTest.php new file mode 100644 index 00000000000..91253d377d2 --- /dev/null +++ b/core/modules/package_manager/tests/src/Unit/ComposerRunnerTest.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\package_manager\Unit; + +use Drupal\Core\File\FileSystemInterface; +use Drupal\package_manager\ComposerRunner; +use Drupal\Tests\UnitTestCase; +use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; +use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use Prophecy\Argument; + +// cspell:ignore BINDIR + +/** + * Tests Package Manager's Composer runner service. + * + * @internal + */ +#[CoversClass(ComposerRunner::class)] +#[Group('package_manager')] +class ComposerRunnerTest extends UnitTestCase { + + /** + * Tests that the Composer runner runs Composer through the PHP interpreter. + */ + public function testRunner(): void { + $executable_finder = $this->prophesize(ExecutableFinderInterface::class); + $executable_finder->find('composer') + ->willReturn('/mock/composer') + ->shouldBeCalled(); + + $process_factory = $this->prophesize(ProcessFactoryInterface::class); + $process_factory->create( + // Internally, ComposerRunner uses Symfony's PhpExecutableFinder to locate + // the PHP interpreter, which should resolve to PHP_BINARY a command-line + // test environment. + [PHP_BINARY, '/mock/composer', '--version'], + NULL, + Argument::withKey('COMPOSER_HOME'), + )->shouldBeCalled(); + + $file_system = $this->prophesize(FileSystemInterface::class); + $file_system->getTempDirectory()->shouldBeCalled(); + $file_system->prepareDirectory(Argument::cetera())->shouldBeCalled(); + + $runner = new ComposerRunner( + $executable_finder->reveal(), + $process_factory->reveal(), + $file_system->reveal(), + $this->getConfigFactoryStub([ + 'system.site' => [ + 'uuid' => 'testing', + ], + ]), + ); + $runner->run(['--version']); + } + +} diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php index ed7adef588f..b393e8ac30c 100644 --- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php +++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php @@ -82,12 +82,28 @@ class PathItem extends FieldItemBase { // If we have an alias, we need to create or update a path alias entity. if ($alias) { - if (!$update || !$pid) { - $path_alias = $path_alias_storage->create([ - 'path' => '/' . $entity->toUrl()->getInternalPath(), - 'alias' => $alias, - 'langcode' => $alias_langcode, - ]); + $properties = [ + 'path' => '/' . $entity->toUrl()->getInternalPath(), + 'alias' => $alias, + 'langcode' => $alias_langcode, + ]; + + if (!$pid) { + // Try to load it from storage before creating it. In some cases the + // path alias could be created before this function runs. For example, + // \Drupal\workspaces\EntityOperations::entityTranslationInsert will + // create a translation, and an associated path alias will be created + // with it. + $query = $path_alias_storage->getQuery()->accessCheck(FALSE); + foreach ($properties as $field => $value) { + $query->condition($field, $value); + } + $ids = $query->execute(); + $pid = $ids ? reset($ids) : $pid; + } + + if (!$pid) { + $path_alias = $path_alias_storage->create($properties); $path_alias->save(); $this->set('pid', $path_alias->id()); } diff --git a/core/modules/path/tests/modules/path_test_misc/path_test_misc.info.yml b/core/modules/path/tests/modules/path_test_misc/path_test_misc.info.yml new file mode 100644 index 00000000000..701a1028914 --- /dev/null +++ b/core/modules/path/tests/modules/path_test_misc/path_test_misc.info.yml @@ -0,0 +1,5 @@ +name: 'Path test miscellaneous utilities' +type: module +description: 'Utilities for path testing' +package: Testing +version: VERSION diff --git a/core/modules/path/tests/modules/path_test_misc/src/Hook/PathTestMiscHooks.php b/core/modules/path/tests/modules/path_test_misc/src/Hook/PathTestMiscHooks.php new file mode 100644 index 00000000000..5fc6662a09d --- /dev/null +++ b/core/modules/path/tests/modules/path_test_misc/src/Hook/PathTestMiscHooks.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\path_test_misc\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\node\NodeInterface; + +/** + * Hook implementations for path_test_misc. + */ +class PathTestMiscHooks { + + /** + * Implements hook_ENTITY_TYPE_presave() for node entities. + * + * This is invoked from testAliasDuplicationPrevention. + */ + #[Hook('node_presave')] + public function nodePresave(NodeInterface $node): void { + if ($node->getTitle() !== 'path duplication test') { + return; + } + + // Update the title to be able to check that this code ran. + $node->setTitle('path duplication test ran'); + + // Create a path alias that has the same values as the one in + // PathItem::postSave. + $path = \Drupal::entityTypeManager()->getStorage('path_alias') + ->create([ + 'path' => '/node/1', + 'alias' => '/my-alias', + 'langcode' => 'en', + ]); + $path->save(); + } + +} diff --git a/core/modules/path/tests/src/Functional/PathNodeFormTest.php b/core/modules/path/tests/src/Functional/PathNodeFormTest.php index 001ba2f09ba..9763a145dc9 100644 --- a/core/modules/path/tests/src/Functional/PathNodeFormTest.php +++ b/core/modules/path/tests/src/Functional/PathNodeFormTest.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace Drupal\Tests\path\Functional; +use Drupal\node\Entity\Node; +use Drupal\node\NodeInterface; + /** * Tests the Path Node form UI. * @@ -14,7 +17,7 @@ class PathNodeFormTest extends PathTestBase { /** * {@inheritdoc} */ - protected static $modules = ['node', 'path']; + protected static $modules = ['node', 'path', 'path_test_misc']; /** * {@inheritdoc} @@ -59,4 +62,27 @@ class PathNodeFormTest extends PathTestBase { $assert_session->fieldNotExists('path[0][alias]'); } + /** + * Tests that duplicate path aliases don't get created. + */ + public function testAliasDuplicationPrevention(): void { + $this->drupalGet('node/add/page'); + $edit['title[0][value]'] = 'path duplication test'; + $edit['path[0][alias]'] = '/my-alias'; + $this->submitForm($edit, 'Save'); + + // Test that PathItem::postSave detects if a path alias exists + // before creating one. + $aliases = \Drupal::entityTypeManager() + ->getStorage('path_alias') + ->loadMultiple(); + static::assertCount(1, $aliases); + $node = Node::load(1); + static::assertInstanceOf(NodeInterface::class, $node); + + // This updated title gets set in PathTestMiscHooks::nodePresave. This + // is a way of ensuring that bit of test code runs. + static::assertEquals('path duplication test ran', $node->getTitle()); + } + } diff --git a/core/modules/system/system.libraries.yml b/core/modules/system/system.libraries.yml index acb02dd1f4b..cd7165bffbb 100644 --- a/core/modules/system/system.libraries.yml +++ b/core/modules/system/system.libraries.yml @@ -8,7 +8,6 @@ base: css/components/clearfix.module.css: { weight: -10 } css/components/hidden.module.css: { weight: -10 } css/components/js.module.css: { weight: -10 } - css/components/reset-appearance.module.css: { weight: -10 } admin: version: VERSION diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml index f9ca25544ab..95b7a1e4c0f 100644 --- a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml +++ b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml @@ -43,6 +43,13 @@ ajax_test.insert_links_inline_wrapper: requirements: _access: 'TRUE' +ajax_test.insert_links_table_wrapper: + path: '/ajax-test/insert-table-wrapper' + defaults: + _controller: '\Drupal\ajax_test\Controller\AjaxTestController::insertLinksTableWrapper' + requirements: + _access: 'TRUE' + ajax_test.dialog_close: path: '/ajax-test/dialog-close' defaults: diff --git a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php index aea5322ce2d..b0adea8e7c7 100644 --- a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php +++ b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php @@ -141,6 +141,36 @@ class AjaxTestController { } /** + * Returns a render array of links that directly Drupal.ajax(). + * + * @return array + * Renderable array of AJAX response contents. + */ + public function insertLinksTableWrapper(): array { + $build['links'] = [ + 'ajax_target' => [ + '#markup' => '<div class="ajax-target-wrapper"><table><tbody id="ajax-target"></tbody></table></div>', + ], + 'links' => [ + '#theme' => 'links', + '#attached' => ['library' => ['ajax_test/ajax_insert']], + ], + ]; + + $build['links']['links']['#links']['table-row'] = [ + 'title' => 'Link table-row', + 'url' => Url::fromRoute('ajax_test.ajax_render_types', ['type' => 'table-row']), + 'attributes' => [ + 'class' => ['ajax-insert'], + 'data-method' => 'html', + 'data-effect' => 'none', + ], + ]; + + return $build; + } + + /** * Returns a render array that will be rendered by AjaxRenderer. * * Verifies that the response incorporates JavaScript settings generated @@ -336,6 +366,7 @@ class AjaxTestController { 'comment-not-wrapped' => '<!-- COMMENT --><div class="comment-not-wrapped">comment-not-wrapped</div>', 'svg' => '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect x="0" y="0" height="10" width="10" fill="green"></rect></svg>', 'empty' => '', + 'table-row' => '<tr><td>table-row</td></tr>', ]; $render_multiple_root = [ 'mixed' => ' foo <!-- COMMENT --> foo bar<div class="a class"><p>some string</p></div> additional not wrapped strings, <!-- ANOTHER COMMENT --> <p>final string</p>', diff --git a/core/modules/system/tests/modules/test_htmx/src/Controller/HtmxTestAttachmentsController.php b/core/modules/system/tests/modules/test_htmx/src/Controller/HtmxTestAttachmentsController.php index 9045a4c8b9c..9027509a19c 100644 --- a/core/modules/system/tests/modules/test_htmx/src/Controller/HtmxTestAttachmentsController.php +++ b/core/modules/system/tests/modules/test_htmx/src/Controller/HtmxTestAttachmentsController.php @@ -23,6 +23,26 @@ final class HtmxTestAttachmentsController extends ControllerBase { } /** + * Builds a response with a `beforebegin` swap. + * + * @return mixed[] + * A render array. + */ + public function before(): array { + return self::generateHtmxButton('beforebegin'); + } + + /** + * Builds a response with an `afterend` swap.. + * + * @return mixed[] + * A render array. + */ + public function after(): array { + return self::generateHtmxButton('afterend'); + } + + /** * Builds the HTMX response. * * @return mixed[] @@ -44,12 +64,22 @@ final class HtmxTestAttachmentsController extends ControllerBase { } /** + * We need a static callback that ignores callback parameters. + * + * @return array + * The render array. + */ + public static function replaceWithAjax(): array { + return static::generateHtmxButton(); + } + + /** * Static helper to for reusable render array. * * @return array * The render array. */ - public static function generateHtmxButton(): array { + public static function generateHtmxButton(string $swap = ''): array { $url = Url::fromRoute('test_htmx.attachments.replace'); $build['replace'] = [ '#type' => 'html_tag', @@ -68,6 +98,9 @@ final class HtmxTestAttachmentsController extends ControllerBase { ], ], ]; + if ($swap !== '') { + $build['replace']['#attributes']['data-hx-swap'] = $swap; + } $build['content'] = [ '#type' => 'container', diff --git a/core/modules/system/tests/modules/test_htmx/src/Form/HtmxTestAjaxForm.php b/core/modules/system/tests/modules/test_htmx/src/Form/HtmxTestAjaxForm.php index 8fffbbc5f40..f812a99582d 100644 --- a/core/modules/system/tests/modules/test_htmx/src/Form/HtmxTestAjaxForm.php +++ b/core/modules/system/tests/modules/test_htmx/src/Form/HtmxTestAjaxForm.php @@ -32,7 +32,7 @@ class HtmxTestAjaxForm extends FormBase { '#ajax' => [ 'callback' => [ HtmxTestAttachmentsController::class, - 'generateHtmxButton', + 'replaceWithAjax', ], 'wrapper' => 'ajax-test-container', ], diff --git a/core/modules/system/tests/modules/test_htmx/test_htmx.routing.yml b/core/modules/system/tests/modules/test_htmx/test_htmx.routing.yml index 406c3027f3b..33dca377c71 100644 --- a/core/modules/system/tests/modules/test_htmx/test_htmx.routing.yml +++ b/core/modules/system/tests/modules/test_htmx/test_htmx.routing.yml @@ -6,6 +6,22 @@ test_htmx.attachments.page: requirements: _permission: 'access content' +test_htmx.attachments.before: + path: '/htmx-test-attachments/before' + defaults: + _title: 'Page' + _controller: '\Drupal\test_htmx\Controller\HtmxTestAttachmentsController::before' + requirements: + _permission: 'access content' + +test_htmx.attachments.after: + path: '/htmx-test-attachments/after' + defaults: + _title: 'Page' + _controller: '\Drupal\test_htmx\Controller\HtmxTestAttachmentsController::after' + requirements: + _permission: 'access content' + test_htmx.attachments.replace: path: '/htmx-test-attachments/replace' defaults: diff --git a/core/modules/views/src/Hook/ViewsHooks.php b/core/modules/views/src/Hook/ViewsHooks.php index b309887723c..5a7b28735f5 100644 --- a/core/modules/views/src/Hook/ViewsHooks.php +++ b/core/modules/views/src/Hook/ViewsHooks.php @@ -375,7 +375,7 @@ class ViewsHooks { #[Hook('view_presave')] public function viewPresave(ViewEntityInterface $view): void { /** @var \Drupal\views\ViewsConfigUpdater $config_updater */ - $config_updater = \Drupal::classResolver(ViewsConfigUpdater::class); + $config_updater = \Drupal::service(ViewsConfigUpdater::class); $config_updater->updateAll($view); } diff --git a/core/modules/views/src/ViewsConfigUpdater.php b/core/modules/views/src/ViewsConfigUpdater.php index 9c5d38e10ac..c6a4fbc62d0 100644 --- a/core/modules/views/src/ViewsConfigUpdater.php +++ b/core/modules/views/src/ViewsConfigUpdater.php @@ -4,106 +4,38 @@ namespace Drupal\views; use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Core\Config\TypedConfigManagerInterface; -use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; /** * Provides a BC layer for modules providing old configurations. * * @internal */ -class ViewsConfigUpdater implements ContainerInjectionInterface { - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * The entity field manager. - * - * @var \Drupal\Core\Entity\EntityFieldManagerInterface - */ - protected $entityFieldManager; - - /** - * The typed config manager. - * - * @var \Drupal\Core\Config\TypedConfigManagerInterface - */ - protected $typedConfigManager; - - /** - * The views data service. - * - * @var \Drupal\views\ViewsData - */ - protected $viewsData; - - /** - * The formatter plugin manager service. - * - * @var \Drupal\Component\Plugin\PluginManagerInterface - */ - protected $formatterPluginManager; +class ViewsConfigUpdater { /** * Flag determining whether deprecations should be triggered. - * - * @var bool */ - protected $deprecationsEnabled = TRUE; + protected bool $deprecationsEnabled = TRUE; /** * Stores which deprecations were triggered. - * - * @var bool */ - protected $triggeredDeprecations = []; + protected array $triggeredDeprecations = []; /** * ViewsConfigUpdater constructor. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager - * The entity field manager. - * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager - * The typed config manager. - * @param \Drupal\views\ViewsData $views_data - * The views data service. - * @param \Drupal\Component\Plugin\PluginManagerInterface $formatter_plugin_manager - * The formatter plugin manager service. */ public function __construct( - EntityTypeManagerInterface $entity_type_manager, - EntityFieldManagerInterface $entity_field_manager, - TypedConfigManagerInterface $typed_config_manager, - ViewsData $views_data, - PluginManagerInterface $formatter_plugin_manager, + private readonly EntityTypeManagerInterface $entityTypeManager, + private readonly EntityFieldManagerInterface $entityFieldManager, + private readonly TypedConfigManagerInterface $typedConfigManager, + private readonly ViewsData $viewsData, + #[Autowire(service: 'plugin.manager.field.formatter')] + private readonly PluginManagerInterface $formatterPluginManager, ) { - $this->entityTypeManager = $entity_type_manager; - $this->entityFieldManager = $entity_field_manager; - $this->typedConfigManager = $typed_config_manager; - $this->viewsData = $views_data; - $this->formatterPluginManager = $formatter_plugin_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity_type.manager'), - $container->get('entity_field.manager'), - $container->get('config.typed'), - $container->get('views.views_data'), - $container->get('plugin.manager.field.formatter') - ); } /** @@ -112,11 +44,18 @@ class ViewsConfigUpdater implements ContainerInjectionInterface { * @param bool $enabled * Whether deprecations should be enabled. */ - public function setDeprecationsEnabled($enabled) { + public function setDeprecationsEnabled(bool $enabled): void { $this->deprecationsEnabled = $enabled; } /** + * Whether deprecations are enabled. + */ + public function areDeprecationsEnabled(): bool { + return $this->deprecationsEnabled; + } + + /** * Performs all required updates. * * @param \Drupal\views\ViewEntityInterface $view @@ -259,7 +198,7 @@ class ViewsConfigUpdater implements ContainerInjectionInterface { } $deprecations_triggered = &$this->triggeredDeprecations['2640994'][$view->id()]; - if ($this->deprecationsEnabled && $changed && !$deprecations_triggered) { + if ($this->areDeprecationsEnabled() && $changed && !$deprecations_triggered) { $deprecations_triggered = TRUE; @trigger_error(sprintf('The update to convert "numeric" arguments to "entity_target_id" for entity reference fields for view "%s" is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Profile, module and theme provided configuration should be updated. See https://www.drupal.org/node/3441945', $view->id()), E_USER_DEPRECATED); } @@ -351,7 +290,7 @@ class ViewsConfigUpdater implements ContainerInjectionInterface { } $deprecations_triggered = &$this->triggeredDeprecations['table_css_class'][$view->id()]; - if ($this->deprecationsEnabled && $changed && !$deprecations_triggered) { + if ($this->areDeprecationsEnabled() && $changed && !$deprecations_triggered) { $deprecations_triggered = TRUE; @trigger_error(sprintf('The update to add a default table CSS class for view "%s" is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Profile, module and theme provided configuration should be updated. See https://www.drupal.org/node/3499943', $view->id()), E_USER_DEPRECATED); } diff --git a/core/modules/views/tests/modules/views_test_config_updater/src/Hook/ViewsTestConfigUpdaterHooks.php b/core/modules/views/tests/modules/views_test_config_updater/src/Hook/ViewsTestConfigUpdaterHooks.php new file mode 100644 index 00000000000..77db1f9adc9 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config_updater/src/Hook/ViewsTestConfigUpdaterHooks.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\views_test_config_updater\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; +use Drupal\views\ViewEntityInterface; +use Drupal\views\ViewsConfigUpdater; +use Symfony\Component\DependencyInjection\Attribute\Autowire; + +/** + * Hooks for the views_test_config_updater module. + */ +class ViewsTestConfigUpdaterHooks { + + public function __construct( + protected readonly ViewsConfigUpdater $viewsConfigUpdater, + #[Autowire(service: 'keyvalue')] + protected readonly KeyValueFactoryInterface $keyValueFactory, + ) { + + } + + /** + * Implements hook_ENTITY_TYPE_presave(). + */ + #[Hook('view_presave')] + public function viewPresave(ViewEntityInterface $view): void { + $this->keyValueFactory->get('views_test_config_updater')->set('deprecations_enabled', $this->viewsConfigUpdater->areDeprecationsEnabled()); + } + +} diff --git a/core/modules/views/tests/modules/views_test_config_updater/views_test_config_updater.info.yml b/core/modules/views/tests/modules/views_test_config_updater/views_test_config_updater.info.yml new file mode 100644 index 00000000000..1efb67ee48f --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config_updater/views_test_config_updater.info.yml @@ -0,0 +1,6 @@ +name: 'Views Test Config Updater' +type: module +package: Testing +version: VERSION +dependencies: + - drupal:views diff --git a/core/modules/views/tests/modules/views_test_config_updater/views_test_config_updater.post_update.php b/core/modules/views/tests/modules/views_test_config_updater/views_test_config_updater.post_update.php new file mode 100644 index 00000000000..db4c780aa8a --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config_updater/views_test_config_updater.post_update.php @@ -0,0 +1,22 @@ +<?php + +/** + * @file + * Post update functions for Views Test Config Updater. + */ + +declare(strict_types=1); + +use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\views\ViewEntityInterface; +use Drupal\views\ViewsConfigUpdater; + +/** + * Test post update to set deprecations disabled. + */ +function views_test_config_updater_post_update_set_deprecations_disabled(?array &$sandbox = NULL): void { + /** @var \Drupal\views\ViewsConfigUpdater $viewsConfigUpdater */ + $viewsConfigUpdater = \Drupal::service(ViewsConfigUpdater::class); + $viewsConfigUpdater->setDeprecationsEnabled(FALSE); + \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', static fn (ViewEntityInterface $view): bool => TRUE); +} diff --git a/core/modules/views/tests/src/Functional/ViewsConfigUpdaterTest.php b/core/modules/views/tests/src/Functional/ViewsConfigUpdaterTest.php new file mode 100644 index 00000000000..dc5b47eece7 --- /dev/null +++ b/core/modules/views/tests/src/Functional/ViewsConfigUpdaterTest.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\views\Functional; + +use Drupal\Core\Database\Database; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\UpdatePathTestTrait; + +/** + * Tests the views config updater service. + * + * @group views + */ +class ViewsConfigUpdaterTest extends BrowserTestBase { + + use UpdatePathTestTrait; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'views', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $connection = Database::getConnection(); + + // Enable views_test_config_updater via the database so post_update hooks + // can run. + $extensions = $connection->select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute() + ->fetchField(); + $extensions = unserialize($extensions); + $extensions['module']['views_test_config_updater'] = 0; + $connection->update('config') + ->fields([ + 'data' => serialize($extensions), + ]) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute(); + } + + /** + * Tests the deprecationsEnabled flag persists from post_update to presave. + * + * @see views_test_config_updater_post_update_set_deprecations_disabled + * @see \Drupal\views_test_config_updater\Hook\ViewsTestConfigUpdaterHooks::viewPresave() + */ + public function testDeprecationsFlagPersists(): void { + $this->assertNull(\Drupal::keyValue('views_test_config_updater')->get('deprecations_enabled')); + + $this->runUpdates(); + + $this->assertFalse(\Drupal::keyValue('views_test_config_updater')->get('deprecations_enabled')); + } + +} diff --git a/core/modules/views/views.api.php b/core/modules/views/views.api.php index 6579f93199b..9d2f23cf4c9 100644 --- a/core/modules/views/views.api.php +++ b/core/modules/views/views.api.php @@ -13,6 +13,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Language\LanguageInterface; use Drupal\views\Plugin\views\cache\CachePluginBase; use Drupal\views\Plugin\views\PluginBase; +use Drupal\views\Plugin\views\query\QueryPluginBase; use Drupal\views\ViewExecutable; /** diff --git a/core/modules/views/views.post_update.php b/core/modules/views/views.post_update.php index 48623fc593e..2dcc15e73e1 100644 --- a/core/modules/views/views.post_update.php +++ b/core/modules/views/views.post_update.php @@ -59,7 +59,7 @@ function views_removed_post_updates(): array { */ function views_post_update_views_data_argument_plugin_id(?array &$sandbox = NULL): void { /** @var \Drupal\views\ViewsConfigUpdater $view_config_updater */ - $view_config_updater = \Drupal::classResolver(ViewsConfigUpdater::class); + $view_config_updater = \Drupal::service(ViewsConfigUpdater::class); $view_config_updater->setDeprecationsEnabled(FALSE); \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function (ViewEntityInterface $view) use ($view_config_updater): bool { return $view_config_updater->needsEntityArgumentUpdate($view); @@ -71,7 +71,7 @@ function views_post_update_views_data_argument_plugin_id(?array &$sandbox = NULL */ function views_post_update_update_remember_role_empty(?array &$sandbox = NULL): void { /** @var \Drupal\views\ViewsConfigUpdater $view_config_updater */ - $view_config_updater = \Drupal::classResolver(ViewsConfigUpdater::class); + $view_config_updater = \Drupal::service(ViewsConfigUpdater::class); $view_config_updater->setDeprecationsEnabled(FALSE); \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function (ViewEntityInterface $view) use ($view_config_updater): bool { return $view_config_updater->needsRememberRolesUpdate($view); @@ -83,7 +83,7 @@ function views_post_update_update_remember_role_empty(?array &$sandbox = NULL): */ function views_post_update_table_css_class(?array &$sandbox = NULL): void { /** @var \Drupal\views\ViewsConfigUpdater $view_config_updater */ - $view_config_updater = \Drupal::classResolver(ViewsConfigUpdater::class); + $view_config_updater = \Drupal::service(ViewsConfigUpdater::class); $view_config_updater->setDeprecationsEnabled(FALSE); \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function (ViewEntityInterface $view) use ($view_config_updater): bool { return $view_config_updater->needsTableCssClassUpdate($view); diff --git a/core/modules/views/views.services.yml b/core/modules/views/views.services.yml index 2a3aa1a16fa..19c5caf92b7 100644 --- a/core/modules/views/views.services.yml +++ b/core/modules/views/views.services.yml @@ -107,3 +107,5 @@ services: tags: - { name: backend_overridable } Drupal\views\Plugin\views\query\CastSqlInterface: '@views.cast_sql' + Drupal\views\ViewsConfigUpdater: + autowire: true diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php index c7142f6b98c..80c5984cddd 100644 --- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php +++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryAuthenticatedPerformanceTest.php @@ -67,7 +67,7 @@ class OpenTelemetryAuthenticatedPerformanceTest extends PerformanceTestBase { 'ScriptCount' => 2, 'ScriptBytes' => 123850, 'StylesheetCount' => 2, - 'StylesheetBytes' => 42000, + 'StylesheetBytes' => 41950, ]; $this->assertMetrics($expected, $performance_data); } diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php index 027d2933a99..e61eea0c30b 100644 --- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php +++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php @@ -58,7 +58,7 @@ class OpenTelemetryFrontPagePerformanceTest extends PerformanceTestBase { 'ScriptCount' => 1, 'ScriptBytes' => 12000, 'StylesheetCount' => 2, - 'StylesheetBytes' => 39750, + 'StylesheetBytes' => 39150, ]; $this->assertMetrics($expected, $performance_data); } @@ -128,7 +128,7 @@ class OpenTelemetryFrontPagePerformanceTest extends PerformanceTestBase { 'ScriptCount' => 1, 'ScriptBytes' => 12000, 'StylesheetCount' => 2, - 'StylesheetBytes' => 39750, + 'StylesheetBytes' => 39150, ]; $this->assertMetrics($expected, $performance_data); } diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php index 3045b218bc4..2077da07d3a 100644 --- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php +++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php @@ -59,7 +59,7 @@ class OpenTelemetryNodePagePerformanceTest extends PerformanceTestBase { 'ScriptCount' => 1, 'ScriptBytes' => 12000, 'StylesheetCount' => 2, - 'StylesheetBytes' => 41350, + 'StylesheetBytes' => 40800, ]; $this->assertMetrics($expected, $performance_data); } @@ -90,7 +90,7 @@ class OpenTelemetryNodePagePerformanceTest extends PerformanceTestBase { 'ScriptCount' => 1, 'ScriptBytes' => 12000, 'StylesheetCount' => 2, - 'StylesheetBytes' => 41350, + 'StylesheetBytes' => 40800, ]; $this->assertMetrics($expected, $performance_data); } @@ -113,7 +113,7 @@ class OpenTelemetryNodePagePerformanceTest extends PerformanceTestBase { $this->assertSession()->pageTextContains('quiche'); $expected = [ - 'QueryCount' => 191, + 'QueryCount' => 190, 'CacheGetCount' => 210, 'CacheSetCount' => 65, 'CacheDeleteCount' => 0, @@ -122,7 +122,7 @@ class OpenTelemetryNodePagePerformanceTest extends PerformanceTestBase { 'ScriptCount' => 1, 'ScriptBytes' => 12000, 'StylesheetCount' => 2, - 'StylesheetBytes' => 41350, + 'StylesheetBytes' => 40800, ]; $this->assertMetrics($expected, $performance_data); } @@ -302,7 +302,6 @@ class OpenTelemetryNodePagePerformanceTest extends PerformanceTestBase { 'SELECT "t".* FROM "node_revision__field_summary" "t" WHERE ("revision_id" IN ("75")) AND ("deleted" = 0) AND ("langcode" IN ("en", "es", "und", "zxx")) ORDER BY "delta" ASC', 'SELECT "t".* FROM "node_revision__field_tags" "t" WHERE ("revision_id" IN ("75")) AND ("deleted" = 0) AND ("langcode" IN ("en", "es", "und", "zxx")) ORDER BY "delta" ASC', 'SELECT "t".* FROM "node_revision__layout_builder__layout" "t" WHERE ("revision_id" IN ("75")) AND ("deleted" = 0) AND ("langcode" IN ("en", "es", "und", "zxx")) ORDER BY "delta" ASC', - 'SELECT "config"."name" AS "name" FROM "config" "config" WHERE ("collection" = "") AND ("name" LIKE "workflows.workflow.%" ESCAPE \'\\\\\') ORDER BY "collection" ASC, "name" ASC', 'SELECT "revision"."vid" AS "vid", "revision"."langcode" AS "langcode", "revision"."revision_uid" AS "revision_uid", "revision"."revision_timestamp" AS "revision_timestamp", "revision"."revision_log" AS "revision_log", "revision"."revision_default" AS "revision_default", "base"."nid" AS "nid", "base"."type" AS "type", "base"."uuid" AS "uuid", CASE "base"."vid" WHEN "revision"."vid" THEN 1 ELSE 0 END AS "isDefaultRevision" FROM "node" "base" INNER JOIN "node_revision" "revision" ON "revision"."nid" = "base"."nid" AND "revision"."vid" IN (75)', 'SELECT "revision".* FROM "node_field_revision" "revision" WHERE ("revision"."vid" IN (75)) AND ("revision"."vid" IN ("75")) ORDER BY "revision"."vid" ASC', 'SELECT "t".* FROM "node_revision__field_cooking_time" "t" WHERE ("revision_id" IN ("75")) AND ("deleted" = 0) AND ("langcode" IN ("en", "es", "und", "zxx")) ORDER BY "delta" ASC', @@ -325,7 +324,7 @@ class OpenTelemetryNodePagePerformanceTest extends PerformanceTestBase { $this->assertSame($expected_queries, $recorded_queries); $expected = [ - 'QueryCount' => 171, + 'QueryCount' => 170, 'CacheGetCount' => 202, 'CacheGetCountByBin' => [ 'page' => 1, @@ -455,7 +454,7 @@ class OpenTelemetryNodePagePerformanceTest extends PerformanceTestBase { 'ScriptCount' => 1, 'ScriptBytes' => 12000, 'StylesheetCount' => 2, - 'StylesheetBytes' => 41200, + 'StylesheetBytes' => 41000, ]; $this->assertMetrics($expected, $performance_data); } diff --git a/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php b/core/tests/Drupal/BuildTests/QuickStart/QuickStartTest.php index c6fc8f18aa5..fbd0473b562 100644 --- a/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php +++ b/core/tests/Drupal/BuildTests/QuickStart/QuickStartTest.php @@ -2,14 +2,18 @@ declare(strict_types=1); -namespace Drupal\Tests\Core\Command; +namespace Drupal\BuildTests\QuickStart; use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks; +use Drupal\BuildTests\Framework\BuildTestBase; use Drupal\Core\Test\TestDatabase; use Drupal\Tests\BrowserTestBase; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; -use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\PreserveGlobalState; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; +use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; @@ -18,15 +22,12 @@ use Symfony\Component\Process\Process; * * These tests are run in a separate process because they load Drupal code via * an include. - * - * @runTestsInSeparateProcesses - * @preserveGlobalState disabled - * @requires extension pdo_sqlite - * - * @group Command - * @group #slow */ -class QuickStartTest extends TestCase { +#[Group('Command')] +#[PreserveGlobalState(FALSE)] +#[RequiresPhpExtension('pdo_sqlite')] +#[RunTestsInSeparateProcesses] +class QuickStartTest extends BuildTestBase { /** * The PHP executable path. diff --git a/core/tests/Drupal/Tests/Core/Recipe/RecipeQuickStartTest.php b/core/tests/Drupal/BuildTests/QuickStart/RecipeQuickStartTest.php index 65bedfff3d0..07d907bb8d0 100644 --- a/core/tests/Drupal/Tests/Core/Recipe/RecipeQuickStartTest.php +++ b/core/tests/Drupal/BuildTests/QuickStart/RecipeQuickStartTest.php @@ -2,14 +2,18 @@ declare(strict_types=1); -namespace Drupal\Tests\Core\Recipe; +namespace Drupal\BuildTests\QuickStart; use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks; +use Drupal\BuildTests\Framework\BuildTestBase; use Drupal\Core\Test\TestDatabase; use Drupal\Tests\BrowserTestBase; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; -use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\PreserveGlobalState; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; +use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; @@ -18,16 +22,13 @@ use Symfony\Component\Process\Process; * * These tests are run in a separate process because they load Drupal code via * an include. - * - * @runTestsInSeparateProcesses - * @preserveGlobalState disabled - * @requires extension pdo_sqlite - * - * @group Command - * @group Recipe - * @group #slow */ -class RecipeQuickStartTest extends TestCase { +#[Group('Command')] +#[Group('Recipe')] +#[PreserveGlobalState(FALSE)] +#[RequiresPhpExtension('pdo_sqlite')] +#[RunTestsInSeparateProcesses] +class RecipeQuickStartTest extends BuildTestBase { /** * The PHP executable path. diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php index 284a3e35fa9..49c7d6987f7 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php @@ -167,6 +167,12 @@ JS; JS; $expected = '<div class="div-wrapper-forever"></div>'; $this->assertInsert('empty', $expected, $custom_wrapper_new_content); + + // Checking inserting table elements. + $expected = '<tr><td>table-row</td></tr>'; + $this->drupalGet('ajax-test/insert-table-wrapper'); + $this->clickLink('Link table-row'); + $this->assertWaitPageContains('<div class="ajax-target-wrapper"><table><tbody id="ajax-target">' . $expected . '</tbody></table></div>'); } /** diff --git a/core/tests/Drupal/Nightwatch/Tests/a11yTestAdmin.js b/core/tests/Drupal/Nightwatch/Tests/a11yTestAdmin.js index ea71f685206..3dc498246aa 100644 --- a/core/tests/Drupal/Nightwatch/Tests/a11yTestAdmin.js +++ b/core/tests/Drupal/Nightwatch/Tests/a11yTestAdmin.js @@ -25,6 +25,30 @@ const testCases = [ { name: 'Structure Page', path: '/admin/structure' }, { name: 'Add content type', path: '/admin/structure/types/add' }, { name: 'Add vocabulary', path: '/admin/structure/taxonomy/add' }, + { + // Tests long breadcrumb for https://drupal.org/i/3223147. + name: 'Manage text format, mobile', + path: '/admin/config/content/formats/manage/restricted_html', + windowSize: { + // Dimensions used by Lighthouse for mobile. + width: 415, + height: 823, + }, + options: { + runOnly: { + type: 'tag', + values: [ + 'wcag2a', + 'wcag2aa', + 'wcag21a', + 'wcag21aa', + 'best-practice', + 'wcag22a', + 'wcag22aa', + ], + }, + }, + }, // @todo remove the skipped rules below in https://drupal.org/i/3318394. { name: 'Structure | Block', @@ -41,6 +65,13 @@ const testCases = [ testCases.forEach((testCase) => { adminTest[`Accessibility - Admin Theme: ${testCase.name}`] = (browser) => { + if (testCase.windowSize) { + browser.setWindowSize( + testCase.windowSize.width, + testCase.windowSize.height, + ); + } + browser.drupalLoginAsAdmin(() => { browser .drupalRelativeURL(testCase.path) diff --git a/core/tests/Drupal/Nightwatch/Tests/htmx/htmxTest.js b/core/tests/Drupal/Nightwatch/Tests/htmx/htmxTest.js index 98916702a88..4705455683d 100644 --- a/core/tests/Drupal/Nightwatch/Tests/htmx/htmxTest.js +++ b/core/tests/Drupal/Nightwatch/Tests/htmx/htmxTest.js @@ -41,8 +41,54 @@ module.exports = { .assert.not.elementPresent(cssSelector) .waitForElementVisible('[name="replace"]', 1000) .click('[name="replace"]') - .waitForElementVisible(elementSelector, 6000) - .waitForElementVisible(elementInitSelector, 6000) + .waitForElementVisible(elementSelector, 1100) + .waitForElementVisible(elementInitSelector, 1100) + .assert.elementPresent(scriptSelector) + .assert.elementPresent(cssSelector); + }, + + 'Swap Before': (browser) => { + // Load the route htmx will use for the request on click and confirm the + // markup we will be looking for is present in the source markup. + browser + .drupalRelativeURL('/htmx-test-attachments/replace') + .waitForElementVisible('body', 1000) + .assert.elementPresent(elementInitSelector); + // Now load the page with the htmx enhanced button and verify the absence + // of the markup to be inserted. Click the button + // and check for inserted javascript and markup. + browser + .drupalRelativeURL('/htmx-test-attachments/before') + .waitForElementVisible('body', 1000) + .assert.not.elementPresent(scriptSelector) + .assert.not.elementPresent(cssSelector) + .waitForElementVisible('[name="replace"]', 1000) + .click('[name="replace"]') + .waitForElementVisible(elementSelector, 1100) + .waitForElementVisible(elementInitSelector, 1100) + .assert.elementPresent(scriptSelector) + .assert.elementPresent(cssSelector); + }, + + 'Swap After': (browser) => { + // Load the route htmx will use for the request on click and confirm the + // markup we will be looking for is present in the source markup. + browser + .drupalRelativeURL('/htmx-test-attachments/replace') + .waitForElementVisible('body', 1000) + .assert.elementPresent(elementInitSelector); + // Now load the page with the htmx enhanced button and verify the absence + // of the markup to be inserted. Click the button + // and check for inserted javascript and markup. + browser + .drupalRelativeURL('/htmx-test-attachments/after') + .waitForElementVisible('body', 1000) + .assert.not.elementPresent(scriptSelector) + .assert.not.elementPresent(cssSelector) + .waitForElementVisible('[name="replace"]', 1000) + .click('[name="replace"]') + .waitForElementVisible(elementSelector, 1100) + .waitForElementVisible(elementInitSelector, 1100) .assert.elementPresent(scriptSelector) .assert.elementPresent(cssSelector); }, @@ -69,8 +115,8 @@ module.exports = { .waitForElementVisible('[name="replace"]', 1000) .pause(1000) .click('[name="replace"]') - .waitForElementVisible(elementSelector, 6000) - .waitForElementVisible(elementInitSelector, 6000) + .waitForElementVisible(elementSelector, 1100) + .waitForElementVisible(elementInitSelector, 1100) .assert.elementPresent(scriptSelector) .assert.elementPresent(cssSelector); }, diff --git a/core/themes/claro/css/components/breadcrumb.css b/core/themes/claro/css/components/breadcrumb.css index 6bfb6fe74ab..b3a84f7200b 100644 --- a/core/themes/claro/css/components/breadcrumb.css +++ b/core/themes/claro/css/components/breadcrumb.css @@ -25,11 +25,12 @@ .breadcrumb__item, .breadcrumb__link { - display: inline; + display: inline-block; -webkit-text-decoration: none; text-decoration: none; color: var(--color-text); font-weight: bold; + line-height: 1.5rem; } .breadcrumb__item + .breadcrumb__item::before { diff --git a/core/themes/claro/css/components/breadcrumb.pcss.css b/core/themes/claro/css/components/breadcrumb.pcss.css index 320fc2c67d1..4c6893ae706 100644 --- a/core/themes/claro/css/components/breadcrumb.pcss.css +++ b/core/themes/claro/css/components/breadcrumb.pcss.css @@ -18,10 +18,11 @@ .breadcrumb__item, .breadcrumb__link { - display: inline; + display: inline-block; text-decoration: none; color: var(--color-text); font-weight: bold; + line-height: 1.5rem; } .breadcrumb__item + .breadcrumb__item::before { diff --git a/core/themes/claro/css/components/tabs.css b/core/themes/claro/css/components/tabs.css index 8c575e2b2ba..16ab6620c4d 100644 --- a/core/themes/claro/css/components/tabs.css +++ b/core/themes/claro/css/components/tabs.css @@ -278,3 +278,9 @@ 0 0 0 calc(var(--tabs--focus-height) + 2px) var(--color-focus); } } + +@media (forced-colors: active) { + .tabs__trigger svg path { + fill: currentColor; + } +} diff --git a/core/themes/claro/css/components/tabs.pcss.css b/core/themes/claro/css/components/tabs.pcss.css index 81dcb7b65d2..51ed8a3f090 100644 --- a/core/themes/claro/css/components/tabs.pcss.css +++ b/core/themes/claro/css/components/tabs.pcss.css @@ -269,3 +269,9 @@ } } } + +@media (forced-colors: active) { + .tabs__trigger svg path { + fill: currentColor; + } +} diff --git a/core/themes/claro/templates/navigation/menu-local-task.html.twig b/core/themes/claro/templates/navigation/menu-local-task.html.twig index b0b6ce08db9..41591f0f5a1 100644 --- a/core/themes/claro/templates/navigation/menu-local-task.html.twig +++ b/core/themes/claro/templates/navigation/menu-local-task.html.twig @@ -26,6 +26,7 @@ <li{{ attributes.addClass(classes) }}> {{ link }} {% if is_active and level == 'primary' %} + {{ attach_library('core/drupal.reset-appearance') }} <button class="reset-appearance tabs__trigger" type="button" aria-label="{{ 'Tabs display toggle'|t }}" data-drupal-nav-tabs-trigger> {% include "@claro/../images/src/hamburger-menu.svg" %} </button> diff --git a/core/themes/olivero/css/components/dropbutton.css b/core/themes/olivero/css/components/dropbutton.css index 3ebcc55b782..fe80d2ff568 100644 --- a/core/themes/olivero/css/components/dropbutton.css +++ b/core/themes/olivero/css/components/dropbutton.css @@ -75,6 +75,10 @@ outline-offset: -2px; } +.dropbutton-toggle button:dir(rtl) { + border-radius: var(--dropbutton--border-radius) 0 0 var(--dropbutton--border-radius); +} + .dropbutton-toggle button::before { display: block; width: var(--sp0-5); @@ -85,14 +89,10 @@ border-bottom: solid 2px var(--dropbutton--outline-color); } -.dropbutton-wrapper.open :is(.dropbutton-toggle button::before) { +.dropbutton-wrapper.open :is(.dropbutton-toggle button)::before { transform: translateY(25%) rotate(225deg); } -.dropbutton-toggle button:dir(rtl) { - border-radius: var(--dropbutton--border-radius) 0 0 var(--dropbutton--border-radius); -} - /* This is the first <li> element in the list of actions. */ .dropbutton-action:first-child { diff --git a/core/themes/olivero/css/components/dropbutton.pcss.css b/core/themes/olivero/css/components/dropbutton.pcss.css index 8437d69a573..04bb51cc4c3 100644 --- a/core/themes/olivero/css/components/dropbutton.pcss.css +++ b/core/themes/olivero/css/components/dropbutton.pcss.css @@ -66,6 +66,10 @@ outline-offset: -2px; } + &:dir(rtl) { + border-radius: var(--dropbutton--border-radius) 0 0 var(--dropbutton--border-radius); + } + &::before { display: block; width: var(--sp0-5); @@ -74,15 +78,13 @@ transform: translateY(-25%) rotate(45deg); border-right: solid 2px var(--dropbutton--outline-color); border-bottom: solid 2px var(--dropbutton--outline-color); + } - .dropbutton-wrapper.open & { + .dropbutton-wrapper.open & { + &::before { transform: translateY(25%) rotate(225deg); } } - - &:dir(rtl) { - border-radius: var(--dropbutton--border-radius) 0 0 var(--dropbutton--border-radius); - } } /* This is the first <li> element in the list of actions. */ diff --git a/core/themes/olivero/css/components/navigation/nav-primary-wide.css b/core/themes/olivero/css/components/navigation/nav-primary-wide.css index 50179032ffc..2192c44529b 100644 --- a/core/themes/olivero/css/components/navigation/nav-primary-wide.css +++ b/core/themes/olivero/css/components/navigation/nav-primary-wide.css @@ -40,7 +40,7 @@ top: 50%; left: 50%; width: calc(100% + var(--sp)); - height: var(--sp3); + height: calc(100% - var(--sp3)); content: ""; transform: translate(-50%, -50%); border: solid 2px var(--color--primary-50); diff --git a/core/themes/olivero/css/components/navigation/nav-primary-wide.pcss.css b/core/themes/olivero/css/components/navigation/nav-primary-wide.pcss.css index db6b05465d5..a594e6e4215 100644 --- a/core/themes/olivero/css/components/navigation/nav-primary-wide.pcss.css +++ b/core/themes/olivero/css/components/navigation/nav-primary-wide.pcss.css @@ -37,7 +37,7 @@ body:not(.is-always-mobile-nav) { top: 50%; left: 50%; width: calc(100% + var(--sp)); - height: var(--sp3); + height: calc(100% - var(--sp3)); content: ""; transform: translate(-50%, -50%); border: solid 2px var(--color--primary-50); diff --git a/core/themes/olivero/css/components/navigation/nav-secondary.css b/core/themes/olivero/css/components/navigation/nav-secondary.css index 3caa30439b8..57bb75ef51e 100644 --- a/core/themes/olivero/css/components/navigation/nav-secondary.css +++ b/core/themes/olivero/css/components/navigation/nav-secondary.css @@ -47,7 +47,6 @@ position: relative; display: inline-flex; align-items: center; - height: var(--sp2); -webkit-text-decoration: none; text-decoration: none; color: inherit; @@ -99,6 +98,7 @@ body:not(.is-always-mobile-nav) .secondary-nav__menu-link:focus { position: relative; outline: 0; + padding-block: var(--sp0-5); } body:not(.is-always-mobile-nav) .secondary-nav__menu-link:focus::before { @@ -106,7 +106,7 @@ top: 50%; left: 50%; width: calc(100% + var(--sp)); - height: var(--sp3); + height: 100%; content: ""; transform: translate(-50%, -50%); border: solid 2px var(--color--primary-50); diff --git a/core/themes/olivero/css/components/navigation/nav-secondary.pcss.css b/core/themes/olivero/css/components/navigation/nav-secondary.pcss.css index eabeeff71a1..84c0bd86faf 100644 --- a/core/themes/olivero/css/components/navigation/nav-secondary.pcss.css +++ b/core/themes/olivero/css/components/navigation/nav-secondary.pcss.css @@ -42,7 +42,6 @@ position: relative; display: inline-flex; align-items: center; - height: var(--sp2); text-decoration: none; color: inherit; @@ -98,13 +97,14 @@ body:not(.is-always-mobile-nav) { &:focus { position: relative; outline: 0; + padding-block: var(--sp0-5); &::before { position: absolute; top: 50%; left: 50%; width: calc(100% + var(--sp)); - height: var(--sp3); + height: 100%; content: ""; transform: translate(-50%, -50%); border: solid 2px var(--color--primary-50); diff --git a/core/themes/olivero/olivero.libraries.yml b/core/themes/olivero/olivero.libraries.yml index c699ebc72c8..e6f0be35688 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.3.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 8ddfb49b87f..479a45aeec0 100644 --- a/core/themes/olivero/olivero.theme +++ b/core/themes/olivero/olivero.theme @@ -365,6 +365,7 @@ function olivero_preprocess_field(&$variables): void { if (in_array($variables['field_type'], $rich_field_types, TRUE)) { $variables['attributes']['class'][] = 'text-content'; + $variables['#attached']['library'][] = 'olivero/olivero.table'; } if ($variables['field_type'] == 'image' && $variables['element']['#view_mode'] == 'full' && !$variables["element"]["#is_multiple"] && $variables['field_name'] !== 'user_picture') { @@ -616,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'; } /** diff --git a/core/themes/stable9/css/system/components/reset-appearance.module.css b/core/themes/stable9/css/core/components/reset-appearance.module.css index 59741a85ce8..59741a85ce8 100644 --- a/core/themes/stable9/css/system/components/reset-appearance.module.css +++ b/core/themes/stable9/css/core/components/reset-appearance.module.css diff --git a/core/themes/stable9/stable9.info.yml b/core/themes/stable9/stable9.info.yml index c079c2b9b47..ffe223b44f0 100644 --- a/core/themes/stable9/stable9.info.yml +++ b/core/themes/stable9/stable9.info.yml @@ -109,6 +109,11 @@ libraries-override: component: misc/vertical-tabs.css: css/core/vertical-tabs.css + core/drupal.reset-appearance: + css: + component: + misc/components/reset-appearance.module.css: css/core/components/reset-appearance.module.css + # Load version 3.0.3 of normalize.css for backwards compatibility. core/normalize: stable9/normalize @@ -244,7 +249,6 @@ libraries-override: css/components/clearfix.module.css: css/system/components/clearfix.module.css css/components/hidden.module.css: css/system/components/hidden.module.css css/components/js.module.css: css/system/components/js.module.css - css/components/reset-appearance.module.css: css/system/components/reset-appearance.module.css system/admin: css: theme: |