Issue #3493070 by penyaskito, griffynh, wim leers, longwave, pdureau, effulgentsia, xjm, phenaproxima, mradcliffe, danielveza, lauriii, catch: SDC `enum` props should have translatable labels: use `meta:enum`

merge-requests/12313/head
Lee Rowlands 2025-06-05 10:06:29 +10:00
parent 6bdfd060b1
commit 9d55d1e60e
No known key found for this signature in database
GPG Key ID: 2B829A3DF9204DC4
17 changed files with 691 additions and 13 deletions

View File

@ -2,6 +2,24 @@
"$id": "https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata-full.schema.json", "$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#", "$schema": "http://json-schema.org/draft-04/schema#",
"$defs": { "$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": { "slotDefinition": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@ -160,7 +178,7 @@
] ]
}, },
"props": { "props": {
"$ref": "http://json-schema.org/draft-04/schema#" "$ref": "#/$defs/propDefinition"
}, },
"slots": { "slots": {
"$ref": "metadata.schema.json#/$defs/slotDefinition" "$ref": "metadata.schema.json#/$defs/slotDefinition"

View File

@ -2,6 +2,24 @@
"$id": "https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json", "$id": "https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json",
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"$defs": { "$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": { "slotDefinition": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@ -208,7 +226,7 @@
] ]
}, },
"props": { "props": {
"$ref": "http://json-schema.org/draft-04/schema#" "$ref": "#/$defs/propDefinition"
}, },
"slots": { "slots": {
"$ref": "#/$defs/slotDefinition" "$ref": "#/$defs/slotDefinition"

View File

@ -13,6 +13,11 @@ class ComponentMetadata {
use StringTranslationTrait; 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. * The absolute path to the component directory.
* *
@ -115,6 +120,7 @@ class ComponentMetadata {
if (str_starts_with($path, $app_root)) { if (str_starts_with($path, $app_root)) {
$path = substr($path, strlen($app_root)); $path = substr($path, strlen($app_root));
} }
$this->id = $metadata_info['id'];
$this->mandatorySchemas = $enforce_schemas; $this->mandatorySchemas = $enforce_schemas;
$this->path = $path; $this->path = $path;
@ -149,7 +155,7 @@ class ComponentMetadata {
private function parseSchemaInfo(array $metadata_info): ?array { private function parseSchemaInfo(array $metadata_info): ?array {
if (empty($metadata_info['props'])) { if (empty($metadata_info['props'])) {
if ($this->mandatorySchemas) { 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; $schema = NULL;
} }
@ -167,6 +173,12 @@ class ComponentMetadata {
$schema_props = $metadata_info['props']; $schema_props = $metadata_info['props'];
foreach ($schema_props['properties'] ?? [] as $name => $prop_schema) { foreach ($schema_props['properties'] ?? [] as $name => $prop_schema) {
$type = $prop_schema['type'] ?? ''; $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([ $schema['properties'][$name]['type'] = array_unique([
...(array) $type, ...(array) $type,
'object', 'object',
@ -197,6 +209,14 @@ class ComponentMetadata {
* The normalized value object. * The normalized value object.
*/ */
public function normalize(): array { 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 [ return [
'path' => $this->path, 'path' => $this->path,
'machineName' => $this->machineName, 'machineName' => $this->machineName,
@ -204,7 +224,42 @@ class ComponentMetadata {
'name' => $this->name, 'name' => $this->name,
'group' => $this->group, 'group' => $this->group,
'variants' => $this->variants, 'variants' => $this->variants,
'meta' => $meta,
]; ];
} }
/**
* Get translated options labels from enumeration.
*
* @param string $propertyName
* The enum property name.
*
* @return array<string, \Drupal\Core\StringTranslation\TranslatableMarkup>
* 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;
}
} }

View File

@ -21,6 +21,9 @@ props:
enum: enum:
- info - info
- success - success
meta:enum:
info: Information
success: Success
slots: slots:
label: label:
type: string type: string

View File

@ -23,6 +23,9 @@ props:
enum: enum:
- ellipsis - ellipsis
- xs - xs
meta:enum:
ellipsis: Ellipsis
xs: 'Extra-small'
extra_classes: extra_classes:
type: array type: array
title: Extra classes. title: Extra classes.
@ -43,6 +46,15 @@ props:
- h5 - h5
- h6 - h6
- span - 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 # Provide a default value
default: h2 default: h2
icon: icon:

View File

@ -37,6 +37,17 @@ props:
- primary - primary
- small-offset - small-offset
- weight--400 - 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: extra_classes:
type: array type: array
title: Extra classes. title: Extra classes.
@ -53,6 +64,11 @@ props:
- a - a
- button - button
- span - span
meta:enum:
a: Link
button: Button
span: Inline
x-translation-context: HTML tag
# Provide a default value # Provide a default value
default: button default: button
icon: icon:

View File

@ -29,6 +29,10 @@ props:
enum: enum:
- '' - ''
- _blank - _blank
meta:enum:
'': 'Open in same window'
_blank: 'Open in a new window'
x-translation-context: Banner link target
image: image:
title: Media Image title: Media Image
description: Background image for the banner. description: Background image for the banner.

View File

@ -7,6 +7,7 @@
<div {{ attributes }}> <div {{ attributes }}>
<div class="component--my-banner--header"> <div class="component--my-banner--header">
<h3>{{ heading }}</h3> <h3>{{ heading }}</h3>
<p>CTA target selected value: {{ componentMetadata.meta.properties.ctaTarget[ctaTarget] }}</p>
{% include 'sdc_test:my-cta' with { text: ctaText, href: ctaHref, target: ctaTarget } only %} {% include 'sdc_test:my-cta' with { text: ctaText, href: ctaHref, target: ctaTarget } only %}
</div> </div>
<div class="component--my-banner--body"> <div class="component--my-banner--body">

View File

@ -24,3 +24,7 @@ props:
- power - power
- like - like
- external - external
meta:enum:
power: 'Power'
like: 'Like'
external: 'External'

View File

@ -17,12 +17,23 @@ props:
type: string type: string
title: URL title: URL
format: uri format: uri
examples:
- https://drupal.org
target: target:
type: string type: string
title: Target title: Target
description: The target for opening the link.
enum: 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: attributes:
type: Drupal\Core\Template\Attribute type: Drupal\Core\Template\Attribute
name: Attributes name: Attributes

View File

@ -6,5 +6,5 @@
{% endif %} {% endif %}
<a {{ attributes }} href="{{ href }}"> <a {{ attributes }} href="{{ href }}">
{{ text }} {{ text }} ({{ componentMetadata.meta.properties.target[target] }})
</a> </a>

View File

@ -24,3 +24,7 @@ props:
- power - power
- like - like
- external - external
meta:enum:
power: 'Power'
like: 'Like'
external: 'External'

View File

@ -27,6 +27,12 @@ props:
- timer - timer
- serves - serves
- difficulty - difficulty
meta:enum:
knife: Knife
timer: Timer
serves: Serves
difficulty: Difficulty
x-translation-context: Umami recipes icon names
slots: slots:
label: label:
type: string type: string

View File

@ -24,6 +24,10 @@ props:
enum: enum:
- article - article
- div - div
meta:enum:
article: Article
div: Container
x-translation-context: HTML tag
# Provide a default value # Provide a default value
default: article default: article

View File

@ -30,6 +30,15 @@ props:
- h5 - h5
- h6 - h6
- span - 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 # Provide a default value
default: h2 default: h2

View File

@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
// cspell:ignore Abre er bânnêh en una nueba bentana la mîmma
namespace Drupal\KernelTests\Components;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\locale\StringInterface;
use Drupal\locale\StringStorageInterface;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\TerminableInterface;
/**
* Tests the component can be translated.
*
* @group sdc
*/
class ComponentTranslationTest extends ComponentKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'sdc_test',
'locale',
'language',
];
/**
* {@inheritdoc}
*/
protected static $themes = ['sdc_theme_test'];
/**
* The locale storage.
*
* @var \Drupal\locale\StringStorageInterface
*/
protected StringStorageInterface $storage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Add a default locale storage for all these tests.
$this->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;
}
}

View File

@ -4,9 +4,11 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Theme\Component; namespace Drupal\Tests\Core\Theme\Component;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Theme\Component\ComponentMetadata; use Drupal\Core\Theme\Component\ComponentMetadata;
use Drupal\Core\Render\Component\Exception\InvalidComponentException; use Drupal\Core\Render\Component\Exception\InvalidComponentException;
use Drupal\Tests\UnitTestCaseTest; use Drupal\Tests\UnitTestCaseTest;
use PHPUnit\Framework\Attributes\DataProvider;
/** /**
* Unit tests for the component metadata class. * 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. * 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); $metadata = new ComponentMetadata($metadata_info, 'foo/', FALSE);
$this->assertSame($expectations['path'], $metadata->path); $this->assertSame($expectations['path'], $metadata->path);
$this->assertSame($expectations['status'], $metadata->status); $this->assertSame($expectations['status'], $metadata->status);
@ -31,18 +36,23 @@ class ComponentMetadataTest extends UnitTestCaseTest {
/** /**
* Tests the correct checks when enforcing schemas or not. * 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) { if ($missing_schema) {
$this->expectException(InvalidComponentException::class); $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.'); $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); new ComponentMetadata($metadata_info, 'foo/', TRUE);
} }
else { else {
if ($expected_exception !== NULL) {
$this->expectException($expected_exception::class);
$this->expectExceptionMessage($expected_exception->getMessage());
}
new ComponentMetadata($metadata_info, 'foo/', TRUE); new ComponentMetadata($metadata_info, 'foo/', TRUE);
$this->expectNotToPerformAssertions(); if ($expected_exception === NULL) {
$this->expectNotToPerformAssertions();
}
} }
} }
@ -71,7 +81,7 @@ class ComponentMetadataTest extends UnitTestCaseTest {
], ],
TRUE, 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', '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
'id' => 'core:my-button', 'id' => 'core:my-button',
@ -136,7 +146,321 @@ class ComponentMetadataTest extends UnitTestCaseTest {
], ],
FALSE, 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'));
}
}
} }