diff --git a/core/assets/schemas/v1/metadata-full.schema.json b/core/assets/schemas/v1/metadata-full.schema.json index ac275eb1bdf..53121eb4d2b 100644 --- a/core/assets/schemas/v1/metadata-full.schema.json +++ b/core/assets/schemas/v1/metadata-full.schema.json @@ -2,6 +2,24 @@ "$id": "https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata-full.schema.json", "$schema": "http://json-schema.org/draft-04/schema#", "$defs": { + "propDefinition": { + "$ref": "http://json-schema.org/draft-04/schema#", + "meta:enum": { + "type": "object", + "minItems": 1, + "uniqueItems": true, + "patternProperties": { + "additionalProperties": false, + "^[a-zA-Z0-9_-]*$": { + "type": "string" + } + } + }, + "x-translation-context": { + "type": "string", + "title": "Translation Context" + } + }, "slotDefinition": { "type": "object", "additionalProperties": false, @@ -160,7 +178,7 @@ ] }, "props": { - "$ref": "http://json-schema.org/draft-04/schema#" + "$ref": "#/$defs/propDefinition" }, "slots": { "$ref": "metadata.schema.json#/$defs/slotDefinition" diff --git a/core/assets/schemas/v1/metadata.schema.json b/core/assets/schemas/v1/metadata.schema.json index 5e5c753360f..b5f52676f3c 100644 --- a/core/assets/schemas/v1/metadata.schema.json +++ b/core/assets/schemas/v1/metadata.schema.json @@ -2,6 +2,24 @@ "$id": "https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json", "$schema": "http://json-schema.org/draft-04/schema#", "$defs": { + "propDefinition": { + "$ref": "http://json-schema.org/draft-04/schema#", + "meta:enum": { + "type": "object", + "minItems": 1, + "uniqueItems": true, + "patternProperties": { + "additionalProperties": false, + "^[a-zA-Z0-9_-]*$": { + "type": "string" + } + } + }, + "x-translation-context": { + "type": "string", + "title": "Translation Context" + } + }, "slotDefinition": { "type": "object", "additionalProperties": false, @@ -208,7 +226,7 @@ ] }, "props": { - "$ref": "http://json-schema.org/draft-04/schema#" + "$ref": "#/$defs/propDefinition" }, "slots": { "$ref": "#/$defs/slotDefinition" diff --git a/core/lib/Drupal/Core/Theme/Component/ComponentMetadata.php b/core/lib/Drupal/Core/Theme/Component/ComponentMetadata.php index e19e759f173..8cdb368e2e9 100644 --- a/core/lib/Drupal/Core/Theme/Component/ComponentMetadata.php +++ b/core/lib/Drupal/Core/Theme/Component/ComponentMetadata.php @@ -13,6 +13,11 @@ class ComponentMetadata { use StringTranslationTrait; + /** + * The ID of the component, in the form of provider:machine_name. + */ + public readonly string $id; + /** * The absolute path to the component directory. * @@ -115,6 +120,7 @@ class ComponentMetadata { if (str_starts_with($path, $app_root)) { $path = substr($path, strlen($app_root)); } + $this->id = $metadata_info['id']; $this->mandatorySchemas = $enforce_schemas; $this->path = $path; @@ -149,7 +155,7 @@ class ComponentMetadata { private function parseSchemaInfo(array $metadata_info): ?array { if (empty($metadata_info['props'])) { if ($this->mandatorySchemas) { - throw new InvalidComponentException(sprintf('The component "%s" does not provide schema information. Schema definitions are mandatory for components declared in modules. For components declared in themes, schema definitions are only mandatory if the "enforce_prop_schemas" key is set to "true" in the theme info file.', $metadata_info['id'])); + throw new InvalidComponentException(sprintf('The component "%s" does not provide schema information. Schema definitions are mandatory for components declared in modules. For components declared in themes, schema definitions are only mandatory if the "enforce_prop_schemas" key is set to "true" in the theme info file.', $this->id)); } $schema = NULL; } @@ -167,6 +173,12 @@ class ComponentMetadata { $schema_props = $metadata_info['props']; foreach ($schema_props['properties'] ?? [] as $name => $prop_schema) { $type = $prop_schema['type'] ?? ''; + if (isset($prop_schema['enum'], $prop_schema['meta:enum'])) { + $enum_keys_diff = array_diff($prop_schema['enum'], array_keys($prop_schema['meta:enum'])); + if (!empty($enum_keys_diff)) { + throw new InvalidComponentException(sprintf('The values for the %s prop enum in component %s must be defined in meta:enum.', $name, $this->id)); + } + } $schema['properties'][$name]['type'] = array_unique([ ...(array) $type, 'object', @@ -197,6 +209,14 @@ class ComponentMetadata { * The normalized value object. */ public function normalize(): array { + $meta = []; + if (!empty($this->schema['properties'])) { + foreach ($this->schema['properties'] as $prop_name => $prop_definition) { + if (!empty($prop_definition['meta:enum'])) { + $meta['properties'][$prop_name] = $this->getEnumOptions($prop_name); + } + } + } return [ 'path' => $this->path, 'machineName' => $this->machineName, @@ -204,7 +224,42 @@ class ComponentMetadata { 'name' => $this->name, 'group' => $this->group, 'variants' => $this->variants, + 'meta' => $meta, ]; } + /** + * Get translated options labels from enumeration. + * + * @param string $propertyName + * The enum property name. + * + * @return array + * An array with enum options as keys and the (non-rendered) + * translated labels as values. + */ + public function getEnumOptions(string $propertyName): array { + $options = []; + if (isset($this->schema['properties'][$propertyName])) { + $prop_definition = $this->schema['properties'][$propertyName]; + if (!empty($prop_definition['enum'])) { + $translation_context = $prop_definition['x-translation-context'] ?? ''; + // We convert ['a', 'b'], into ['a' => t('a'), 'b' => t('b')]. + $options = array_combine( + $prop_definition['enum'], + // @phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString + array_map(fn($value) => $this->t($value, [], ['context' => $translation_context]), $prop_definition['enum']), + ); + if (!empty($prop_definition['meta:enum'])) { + foreach ($prop_definition['meta:enum'] as $enum_value => $enum_label) { + $options[$enum_value] = + // @phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString + $this->t($enum_label, [], ['context' => $translation_context]); + } + } + } + } + return $options; + } + } diff --git a/core/modules/navigation/components/badge/badge.component.yml b/core/modules/navigation/components/badge/badge.component.yml index a7bb04f963e..a7940d1efa5 100644 --- a/core/modules/navigation/components/badge/badge.component.yml +++ b/core/modules/navigation/components/badge/badge.component.yml @@ -21,6 +21,9 @@ props: enum: - info - success + meta:enum: + info: Information + success: Success slots: label: type: string diff --git a/core/modules/navigation/components/title/title.component.yml b/core/modules/navigation/components/title/title.component.yml index 696960d455f..1001c42c109 100644 --- a/core/modules/navigation/components/title/title.component.yml +++ b/core/modules/navigation/components/title/title.component.yml @@ -23,6 +23,9 @@ props: enum: - ellipsis - xs + meta:enum: + ellipsis: Ellipsis + xs: 'Extra-small' extra_classes: type: array title: Extra classes. @@ -43,6 +46,15 @@ props: - h5 - h6 - span + meta:enum: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + span: Inline + x-translation-context: HTML tag # Provide a default value default: h2 icon: diff --git a/core/modules/navigation/components/toolbar-button/toolbar-button.component.yml b/core/modules/navigation/components/toolbar-button/toolbar-button.component.yml index 87e29a14bf4..130cca22e1c 100644 --- a/core/modules/navigation/components/toolbar-button/toolbar-button.component.yml +++ b/core/modules/navigation/components/toolbar-button/toolbar-button.component.yml @@ -37,6 +37,17 @@ props: - primary - small-offset - weight--400 + meta:enum: + collapsible: Collapsible + dark: Dark + expand--down: Expands down + expand--side: Expands to the side + large: Large + non-interactive: Non-Interactive + primary: Primary + small-offset: Small offset + weight--400: Weight 400 + x-translation-context: Toolbar button modifiers extra_classes: type: array title: Extra classes. @@ -53,6 +64,11 @@ props: - a - button - span + meta:enum: + a: Link + button: Button + span: Inline + x-translation-context: HTML tag # Provide a default value default: button icon: diff --git a/core/modules/system/tests/modules/sdc_test/components/my-banner/my-banner.component.yml b/core/modules/system/tests/modules/sdc_test/components/my-banner/my-banner.component.yml index 195903e9ee7..0c89a0740a6 100644 --- a/core/modules/system/tests/modules/sdc_test/components/my-banner/my-banner.component.yml +++ b/core/modules/system/tests/modules/sdc_test/components/my-banner/my-banner.component.yml @@ -29,6 +29,10 @@ props: enum: - '' - _blank + meta:enum: + '': 'Open in same window' + _blank: 'Open in a new window' + x-translation-context: Banner link target image: title: Media Image description: Background image for the banner. diff --git a/core/modules/system/tests/modules/sdc_test/components/my-banner/my-banner.twig b/core/modules/system/tests/modules/sdc_test/components/my-banner/my-banner.twig index c99fead737a..2bd75f35382 100644 --- a/core/modules/system/tests/modules/sdc_test/components/my-banner/my-banner.twig +++ b/core/modules/system/tests/modules/sdc_test/components/my-banner/my-banner.twig @@ -7,6 +7,7 @@

{{ heading }}

+

CTA target selected value: {{ componentMetadata.meta.properties.ctaTarget[ctaTarget] }}

{% include 'sdc_test:my-cta' with { text: ctaText, href: ctaHref, target: ctaTarget } only %}
diff --git a/core/modules/system/tests/modules/sdc_test/components/my-button/my-button.component.yml b/core/modules/system/tests/modules/sdc_test/components/my-button/my-button.component.yml index 65b3c472096..d0d4f8c73b4 100644 --- a/core/modules/system/tests/modules/sdc_test/components/my-button/my-button.component.yml +++ b/core/modules/system/tests/modules/sdc_test/components/my-button/my-button.component.yml @@ -24,3 +24,7 @@ props: - power - like - external + meta:enum: + power: 'Power' + like: 'Like' + external: 'External' diff --git a/core/modules/system/tests/modules/sdc_test/components/my-cta/my-cta.component.yml b/core/modules/system/tests/modules/sdc_test/components/my-cta/my-cta.component.yml index 889dfe88520..6d16be49cf7 100644 --- a/core/modules/system/tests/modules/sdc_test/components/my-cta/my-cta.component.yml +++ b/core/modules/system/tests/modules/sdc_test/components/my-cta/my-cta.component.yml @@ -17,12 +17,23 @@ props: type: string title: URL format: uri + examples: + - https://drupal.org target: type: string title: Target + description: The target for opening the link. enum: - '' - - _blank + - '_blank' + meta:enum: + '': 'Open in same window' + _blank: 'Open in a new window' + x-translation-context: CTA link target + default: '' + examples: + - '' + - '_blank' attributes: type: Drupal\Core\Template\Attribute name: Attributes diff --git a/core/modules/system/tests/modules/sdc_test/components/my-cta/my-cta.twig b/core/modules/system/tests/modules/sdc_test/components/my-cta/my-cta.twig index e1a326ccf07..afc40f7bd54 100644 --- a/core/modules/system/tests/modules/sdc_test/components/my-cta/my-cta.twig +++ b/core/modules/system/tests/modules/sdc_test/components/my-cta/my-cta.twig @@ -6,5 +6,5 @@ {% endif %} - {{ text }} + {{ text }} ({{ componentMetadata.meta.properties.target[target] }}) diff --git a/core/modules/system/tests/modules/sdc_test_replacements/components/my-button/my-button.component.yml b/core/modules/system/tests/modules/sdc_test_replacements/components/my-button/my-button.component.yml index b87f1180111..053387cf9c4 100644 --- a/core/modules/system/tests/modules/sdc_test_replacements/components/my-button/my-button.component.yml +++ b/core/modules/system/tests/modules/sdc_test_replacements/components/my-button/my-button.component.yml @@ -24,3 +24,7 @@ props: - power - like - external + meta:enum: + power: 'Power' + like: 'Like' + external: 'External' diff --git a/core/profiles/demo_umami/themes/umami/components/badge/badge.component.yml b/core/profiles/demo_umami/themes/umami/components/badge/badge.component.yml index 1d68092cc07..dbe75182edf 100644 --- a/core/profiles/demo_umami/themes/umami/components/badge/badge.component.yml +++ b/core/profiles/demo_umami/themes/umami/components/badge/badge.component.yml @@ -27,6 +27,12 @@ props: - timer - serves - difficulty + meta:enum: + knife: Knife + timer: Timer + serves: Serves + difficulty: Difficulty + x-translation-context: Umami recipes icon names slots: label: type: string diff --git a/core/profiles/demo_umami/themes/umami/components/card/card.component.yml b/core/profiles/demo_umami/themes/umami/components/card/card.component.yml index 58dbae54008..96183ac24bc 100644 --- a/core/profiles/demo_umami/themes/umami/components/card/card.component.yml +++ b/core/profiles/demo_umami/themes/umami/components/card/card.component.yml @@ -24,6 +24,10 @@ props: enum: - article - div + meta:enum: + article: Article + div: Container + x-translation-context: HTML tag # Provide a default value default: article diff --git a/core/profiles/demo_umami/themes/umami/components/title/title.component.yml b/core/profiles/demo_umami/themes/umami/components/title/title.component.yml index df26ee3abf8..007a82b7361 100644 --- a/core/profiles/demo_umami/themes/umami/components/title/title.component.yml +++ b/core/profiles/demo_umami/themes/umami/components/title/title.component.yml @@ -30,6 +30,15 @@ props: - h5 - h6 - span + meta:enum: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + span: Inline + x-translation-context: HTML tag # Provide a default value default: h2 diff --git a/core/tests/Drupal/KernelTests/Components/ComponentTranslationTest.php b/core/tests/Drupal/KernelTests/Components/ComponentTranslationTest.php new file mode 100644 index 00000000000..4495e8fda6a --- /dev/null +++ b/core/tests/Drupal/KernelTests/Components/ComponentTranslationTest.php @@ -0,0 +1,189 @@ +storage = $this->container->get('locale.storage'); + ConfigurableLanguage::createFromLangcode('epa')->save(); + $this->container->get('string_translation')->setDefaultLangcode('epa'); + $this->installSchema('locale', [ + 'locales_location', + 'locales_source', + 'locales_target', + ]); + } + + /** + * Test that components render enum props correctly with their translations. + */ + public function testEnumPropsCanBeTranslated(): void { + $bannerString = $this->buildSourceString(['source' => 'Open in a new window', 'context' => 'Banner link target']); + $bannerString->save(); + $ctaString = $this->buildSourceString(['source' => 'Open in a new window', 'context' => 'CTA link target']); + $ctaString->save(); + $ctaEmptyString = $this->buildSourceString(['source' => 'Open in same window', 'context' => 'CTA link target']); + $ctaEmptyString->save(); + $this->createTranslation($bannerString, 'epa', ['translation' => 'Abre er bânnêh en una nueba bentana']); + $this->createTranslation($ctaString, 'epa', ['translation' => 'Abre er CTA en una nueba bentana']); + $this->createTranslation($ctaEmptyString, 'epa', ['translation' => 'Abre er CTA en la mîmma bentana']); + + $build = [ + 'banner' => [ + '#type' => 'component', + '#component' => 'sdc_test:my-banner', + '#props' => [ + 'heading' => 'I am a banner', + 'ctaText' => 'Click me', + 'ctaHref' => 'https://www.example.org', + 'ctaTarget' => '_blank', + ], + ], + 'cta' => [ + '#type' => 'component', + '#component' => 'sdc_test:my-cta', + '#props' => [ + 'text' => 'Click me', + 'href' => 'https://www.example.org', + 'target' => '_blank', + ], + ], + 'cta_with_empty_enum' => [ + '#type' => 'component', + '#component' => 'sdc_test:my-cta', + '#props' => [ + 'text' => 'Click me', + 'href' => 'https://www.example.org', + 'target' => '', + ], + ], + ]; + \Drupal::state()->set('sdc_test_component', $build); + $response = $this->request(Request::create('sdc-test-component')); + $crawler = new Crawler($response->getContent()); + + // Assert that even if the source is the same, the translations depend on + // the enum context. + $this->assertStringContainsString('Abre er bânnêh en una nueba bentana', $crawler->filter('#sdc-wrapper [data-component-id="sdc_test:my-banner"]')->outerHtml()); + $this->assertStringContainsString('Abre er CTA en una nueba bentana', $crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta"]:nth-of-type(1)')->outerHtml()); + $this->assertStringContainsString('Abre er CTA en la mîmma bentana', $crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta"]:nth-of-type(2)')->outerHtml()); + } + + /** + * Creates random source string object. + * + * @param array $values + * The values array. + * + * @return \Drupal\locale\StringInterface + * A locale string. + */ + protected function buildSourceString(array $values = []): StringInterface { + return $this->storage->createString($values += [ + 'source' => $this->randomMachineName(100), + 'context' => $this->randomMachineName(20), + ]); + } + + /** + * Creates single translation for source string. + * + * @param \Drupal\locale\StringInterface $source + * The source string. + * @param string $langcode + * The language code. + * @param array $values + * The values array. + * + * @return \Drupal\locale\StringInterface + * The translated string object. + */ + protected function createTranslation(StringInterface $source, $langcode, array $values = []): StringInterface { + return $this->storage->createTranslation($values + [ + 'lid' => $source->lid, + 'language' => $langcode, + 'translation' => $this->randomMachineName(100), + ])->save(); + } + + /** + * Passes a request to the HTTP kernel and returns a response. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. + * + * @return \Symfony\Component\HttpFoundation\Response + * The response. + */ + protected function request(Request $request): Response { + // @todo We should replace this when https://drupal.org/i/3390193 lands. + // Reset the request stack. + // \Drupal\KernelTests\KernelTestBase::bootKernel() pushes a bogus request + // to boot the kernel, but it is also needed for any URL generation in tests + // to work. We also need to reset the request stack every time we make a + // request. + $request_stack = $this->container->get('request_stack'); + while ($request_stack->getCurrentRequest() !== NULL) { + $request_stack->pop(); + } + + $http_kernel = $this->container->get('http_kernel'); + self::assertInstanceOf(HttpKernelInterface::class, $http_kernel); + $response = $http_kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, FALSE); + $content = $response->getContent(); + self::assertNotFalse($content); + $this->setRawContent($content); + + self::assertInstanceOf(TerminableInterface::class, $http_kernel); + $http_kernel->terminate($request, $response); + + return $response; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php index 4e0555eb2ae..6d24143339e 100644 --- a/core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php +++ b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php @@ -4,9 +4,11 @@ declare(strict_types=1); namespace Drupal\Tests\Core\Theme\Component; +use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Theme\Component\ComponentMetadata; use Drupal\Core\Render\Component\Exception\InvalidComponentException; use Drupal\Tests\UnitTestCaseTest; +use PHPUnit\Framework\Attributes\DataProvider; /** * Unit tests for the component metadata class. @@ -18,10 +20,13 @@ class ComponentMetadataTest extends UnitTestCaseTest { /** * Tests that the correct data is returned for each property. - * - * @dataProvider dataProviderMetadata */ - public function testMetadata(array $metadata_info, array $expectations): void { + #[DataProvider('dataProviderMetadata')] + public function testMetadata(array $metadata_info, array $expectations, bool $missing_schema, ?\Throwable $expectedException = NULL): void { + if ($expectedException !== NULL) { + $this->expectException($expectedException::class); + $this->expectExceptionMessage($expectedException->getMessage()); + } $metadata = new ComponentMetadata($metadata_info, 'foo/', FALSE); $this->assertSame($expectations['path'], $metadata->path); $this->assertSame($expectations['status'], $metadata->status); @@ -31,18 +36,23 @@ class ComponentMetadataTest extends UnitTestCaseTest { /** * Tests the correct checks when enforcing schemas or not. - * - * @dataProvider dataProviderMetadata */ - public function testMetadataEnforceSchema(array $metadata_info, array $expectations, bool $missing_schema): void { + #[DataProvider('dataProviderMetadata')] + public function testMetadataEnforceSchema(array $metadata_info, array $expectations, bool $missing_schema, ?\Throwable $expected_exception = NULL): void { if ($missing_schema) { $this->expectException(InvalidComponentException::class); $this->expectExceptionMessage('The component "' . $metadata_info['id'] . '" does not provide schema information. Schema definitions are mandatory for components declared in modules. For components declared in themes, schema definitions are only mandatory if the "enforce_prop_schemas" key is set to "true" in the theme info file.'); new ComponentMetadata($metadata_info, 'foo/', TRUE); } else { + if ($expected_exception !== NULL) { + $this->expectException($expected_exception::class); + $this->expectExceptionMessage($expected_exception->getMessage()); + } new ComponentMetadata($metadata_info, 'foo/', TRUE); - $this->expectNotToPerformAssertions(); + if ($expected_exception === NULL) { + $this->expectNotToPerformAssertions(); + } } } @@ -71,7 +81,7 @@ class ComponentMetadataTest extends UnitTestCaseTest { ], TRUE, ], - 'complete example with schema' => [ + 'complete example with schema, but no meta:enum' => [ [ '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json', 'id' => 'core:my-button', @@ -136,7 +146,321 @@ class ComponentMetadataTest extends UnitTestCaseTest { ], FALSE, ], + 'complete example with schema, but no matching meta:enum' => [ + [ + '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json', + 'id' => 'core:my-button', + 'machineName' => 'my-button', + 'path' => 'foo/my-other/path', + 'name' => 'Button', + 'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.', + 'libraryOverrides' => ['dependencies' => ['core/drupal']], + 'group' => 'my-group', + 'props' => [ + 'type' => 'object', + 'required' => ['text'], + 'properties' => [ + 'text' => [ + 'type' => 'string', + 'title' => 'Title', + 'description' => 'The title for the button', + 'minLength' => 2, + 'examples' => ['Press', 'Submit now'], + ], + 'iconType' => [ + 'type' => 'string', + 'title' => 'Icon Type', + 'enum' => [ + 'power', + 'like', + 'external', + ], + 'meta:enum' => [ + 'power' => 'Power', + 'fav' => 'Favorite', + 'external' => 'External', + ], + ], + ], + ], + ], + [ + 'path' => 'my-other/path', + 'status' => 'stable', + 'thumbnail' => '', + 'group' => 'my-group', + 'additionalProperties' => FALSE, + 'props' => [ + 'type' => 'object', + 'required' => ['text'], + 'additionalProperties' => FALSE, + 'properties' => [ + 'text' => [ + 'type' => ['string', 'object'], + 'title' => 'Title', + 'description' => 'The title for the button', + 'minLength' => 2, + 'examples' => ['Press', 'Submit now'], + ], + 'iconType' => [ + 'type' => ['string', 'object'], + 'title' => 'Icon Type', + 'enum' => [ + 'power', + 'like', + 'external', + ], + 'meta:enum' => [ + 'power' => 'Power', + 'fav' => 'Favorite', + 'external' => 'External', + ], + ], + ], + ], + ], + FALSE, + new InvalidComponentException('The values for the iconType prop enum in component core:my-button must be defined in meta:enum.'), + ], + 'complete example with schema (including meta:enum)' => [ + [ + '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json', + 'id' => 'core:my-button', + 'machineName' => 'my-button', + 'path' => 'foo/my-other/path', + 'name' => 'Button', + 'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.', + 'libraryOverrides' => ['dependencies' => ['core/drupal']], + 'group' => 'my-group', + 'props' => [ + 'type' => 'object', + 'required' => ['text'], + 'properties' => [ + 'text' => [ + 'type' => 'string', + 'title' => 'Title', + 'description' => 'The title for the button', + 'minLength' => 2, + 'examples' => ['Press', 'Submit now'], + ], + 'iconType' => [ + 'type' => 'string', + 'title' => 'Icon Type', + 'enum' => [ + 'power', + 'like', + 'external', + ], + 'meta:enum' => [ + 'power' => 'Power', + 'like' => 'Like', + 'external' => 'External', + ], + ], + ], + ], + ], + [ + 'path' => 'my-other/path', + 'status' => 'stable', + 'thumbnail' => '', + 'group' => 'my-group', + 'additionalProperties' => FALSE, + 'props' => [ + 'type' => 'object', + 'required' => ['text'], + 'additionalProperties' => FALSE, + 'properties' => [ + 'text' => [ + 'type' => ['string', 'object'], + 'title' => 'Title', + 'description' => 'The title for the button', + 'minLength' => 2, + 'examples' => ['Press', 'Submit now'], + ], + 'iconType' => [ + 'type' => ['string', 'object'], + 'title' => 'Icon Type', + 'enum' => [ + 'power', + 'like', + 'external', + ], + 'meta:enum' => [ + 'power' => 'Power', + 'like' => 'Like', + 'external' => 'External', + ], + ], + ], + ], + ], + FALSE, + ], + 'complete example with schema (including meta:enum and x-translation-context)' => [ + [ + '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json', + 'id' => 'core:my-button', + 'machineName' => 'my-button', + 'path' => 'foo/my-other/path', + 'name' => 'Button', + 'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.', + 'libraryOverrides' => ['dependencies' => ['core/drupal']], + 'group' => 'my-group', + 'props' => [ + 'type' => 'object', + 'required' => ['text'], + 'properties' => [ + 'text' => [ + 'type' => 'string', + 'title' => 'Title', + 'description' => 'The title for the button', + 'minLength' => 2, + 'examples' => ['Press', 'Submit now'], + ], + 'iconType' => [ + 'type' => 'string', + 'title' => 'Icon Type', + 'enum' => [ + 'power', + 'like', + 'external', + ], + 'meta:enum' => [ + 'power' => 'Power', + 'like' => 'Like', + 'external' => 'External', + ], + 'x-translation-context' => 'Icon Type', + ], + ], + ], + ], + [ + 'path' => 'my-other/path', + 'status' => 'stable', + 'thumbnail' => '', + 'group' => 'my-group', + 'additionalProperties' => FALSE, + 'props' => [ + 'type' => 'object', + 'required' => ['text'], + 'additionalProperties' => FALSE, + 'properties' => [ + 'text' => [ + 'type' => ['string', 'object'], + 'title' => 'Title', + 'description' => 'The title for the button', + 'minLength' => 2, + 'examples' => ['Press', 'Submit now'], + ], + 'iconType' => [ + 'type' => ['string', 'object'], + 'title' => 'Icon Type', + 'enum' => [ + 'power', + 'like', + 'external', + ], + 'meta:enum' => [ + 'power' => 'Power', + 'like' => 'Like', + 'external' => 'External', + ], + 'x-translation-context' => 'Icon Type', + ], + ], + ], + ], + FALSE, + ], ]; } + public static function dataProviderEnumOptionsMetadata(): array { + $common_schema = [ + '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json', + 'id' => 'core:my-button', + 'machineName' => 'my-button', + 'path' => 'foo/my-other/path', + 'name' => 'Button', + ]; + return [ + 'no meta:enum' => [$common_schema + + [ + 'props' => [ + 'type' => 'object', + 'required' => ['text'], + 'properties' => [ + 'iconType' => [ + 'type' => 'string', + 'title' => 'Icon Type', + 'enum' => [ + 'power', + 'like', + 'external', + ], + ], + ], + ], + ], + 'iconType', + [ + 'power' => 'power', + 'like' => 'like', + 'external' => 'external', + ], + '', + ], + 'meta:enum, with x-translation-context' => [$common_schema + + [ + 'props' => [ + 'type' => 'object', + 'required' => ['text'], + 'properties' => [ + 'target' => [ + 'type' => 'string', + 'title' => 'Icon Type', + 'enum' => [ + '', + '_blank', + ], + 'meta:enum' => [ + '' => 'Opens in same window', + '_blank' => 'Opens in new window', + ], + 'x-translation-context' => 'Link target', + ], + ], + ], + ], + 'target', + [ + '' => 'Opens in same window', + '_blank' => 'Opens in new window', + ], + 'Link target', + ], + ]; + } + + /** + * @covers ::getEnumOptions + */ + #[DataProvider('dataProviderEnumOptionsMetadata')] + public function testGetEnumOptions(array $metadata_info, string $prop_name, array $expected_values, string $expected_context): void { + $translation = $this->getStringTranslationStub(); + $container = new ContainerBuilder(); + $container->set('string_translation', $translation); + \Drupal::setContainer($container); + + $component_metadata = new ComponentMetadata($metadata_info, 'foo/', TRUE); + $options = $component_metadata->getEnumOptions($prop_name); + $rendered_options = array_map(fn($value) => (string) $value, $options); + $this->assertSame($expected_values, $rendered_options); + foreach ($options as $translatable) { + $this->assertSame($expected_context, $translatable->getOption('context')); + } + } + }