diff --git a/src/internal/analytics-metadata/__tests__/metadata-utils.test.ts b/src/internal/analytics-metadata/__tests__/metadata-utils.test.ts
index a4d9021..0df4d45 100644
--- a/src/internal/analytics-metadata/__tests__/metadata-utils.test.ts
+++ b/src/internal/analytics-metadata/__tests__/metadata-utils.test.ts
@@ -337,6 +337,110 @@ describe('processMetadata', () => {
document.body.removeChild(mockDiv);
});
+
+ test('extracts component description from aria-describedby on label element', () => {
+ const mockDiv = document.createElement('div');
+ mockDiv.innerHTML = `
+ Must be 3-20 characters
+
+ `;
+ document.body.appendChild(mockDiv);
+
+ const result: any = processMetadata(mockDiv, { name: 'awsui.Input', label: 'input' });
+ expect(result.description).toBe('Must be 3-20 characters');
+
+ document.body.removeChild(mockDiv);
+ });
+
+ test('concatenates multiple aria-describedby IDs', () => {
+ const mockDiv = document.createElement('div');
+ mockDiv.innerHTML = `
+ Enter your work email
+ Must end with @amazon.com
+
+ `;
+ document.body.appendChild(mockDiv);
+
+ const result: any = processMetadata(mockDiv, { name: 'awsui.Input', label: 'input' });
+ expect(result.description).toBe('Enter your work email Must end with @amazon.com');
+
+ document.body.removeChild(mockDiv);
+ });
+
+ test('does not extract description when label element has no aria-describedby', () => {
+ const mockDiv = document.createElement('div');
+ mockDiv.innerHTML = `
+
+
This is a description
+
+ `;
+ document.body.appendChild(mockDiv);
+
+ const result: any = processMetadata(mockDiv, { name: 'awsui.FormField', label: '.label' });
+ expect(result.description).toBeUndefined();
+
+ document.body.removeChild(mockDiv);
+ });
+
+ test('does not extract description when component has no label selector', () => {
+ const mockDiv = document.createElement('div');
+ mockDiv.innerHTML = `
+ Some description
+
+ `;
+ document.body.appendChild(mockDiv);
+
+ const result: any = processMetadata(mockDiv, { name: 'awsui.Input' });
+ expect(result.description).toBeUndefined();
+
+ document.body.removeChild(mockDiv);
+ });
+
+ test('does not extract description from child elements', () => {
+ const mockDiv = document.createElement('div');
+ mockDiv.innerHTML = `
+ This is a description
+
+ `;
+ document.body.appendChild(mockDiv);
+
+ const result: any = processMetadata(mockDiv, { name: 'awsui.FormField', label: '.label' });
+ expect(result.description).toBeUndefined();
+
+ document.body.removeChild(mockDiv);
+ });
+
+ test('skips missing IDs in aria-describedby', () => {
+ const mockDiv = document.createElement('div');
+ mockDiv.innerHTML = `
+ Constraint text
+
+ `;
+ document.body.appendChild(mockDiv);
+
+ const result: any = processMetadata(mockDiv, { name: 'awsui.Input', label: 'input' });
+ expect(result.description).toBe('Constraint text');
+
+ document.body.removeChild(mockDiv);
+ });
+
+ test('extracts description when label is a LabelIdentifier with array selector', () => {
+ const mockDiv = document.createElement('div');
+ mockDiv.innerHTML = `
+ Help text
+
+
+ `;
+ document.body.appendChild(mockDiv);
+
+ const result: any = processMetadata(mockDiv, {
+ name: 'awsui.Select',
+ label: { selector: ['.header', '.trigger'] },
+ });
+ expect(result.description).toBe('Help text');
+
+ document.body.removeChild(mockDiv);
+ });
});
describe('merge', () => {
diff --git a/src/internal/analytics-metadata/metadata-utils.ts b/src/internal/analytics-metadata/metadata-utils.ts
index 6766283..f1cc3b5 100644
--- a/src/internal/analytics-metadata/metadata-utils.ts
+++ b/src/internal/analytics-metadata/metadata-utils.ts
@@ -22,7 +22,7 @@ export const processMetadata = (
localMetadata: any,
options?: GetComponentsTreeOptions
): GeneratedAnalyticsMetadataFragment => {
- return Object.keys(localMetadata).reduce((acc: any, key: string) => {
+ const result: any = Object.keys(localMetadata).reduce((acc: any, key: string) => {
if (key.toLowerCase().match(/labels$/)) {
acc[key] = processLabel(node, localMetadata[key], 'multi');
} else if (key.toLowerCase().match(/label$/)) {
@@ -68,6 +68,15 @@ export const processMetadata = (
}
return acc;
}, {});
+
+ if (result.name && node && localMetadata.label) {
+ const description = resolveComponentDescription(node, localMetadata.label);
+ if (description) {
+ result.description = description;
+ }
+ }
+
+ return result;
};
const isNil = (value: any) => {
@@ -159,6 +168,31 @@ const resolveInputDescription = (root: HTMLElement, input: HTMLElement): string
return '';
};
+const resolveComponentDescription = (node: HTMLElement, label: string | { selector?: string | string[] }): string => {
+ const selectors =
+ typeof label === 'string' ? [label] : Array.isArray(label.selector) ? label.selector : [label.selector || ''];
+ const doc = node.ownerDocument || document;
+ for (const selector of selectors) {
+ const el = selector ? node.querySelector(selector) : node;
+ if (!el) {
+ continue;
+ }
+ const describedBy = el.getAttribute('aria-describedby');
+ if (!describedBy) {
+ continue;
+ }
+ const description = describedBy
+ .split(' ')
+ .map(id => doc.getElementById(id)?.textContent?.trim() || '')
+ .filter(Boolean)
+ .join(' ');
+ if (description) {
+ return description;
+ }
+ }
+ return '';
+};
+
const getRadioGroupOptions = (node: HTMLElement): Array => {
const inputs = Array.from(node.querySelectorAll('input[type="radio"]')) as HTMLElement[];
return inputs
diff --git a/src/internal/analytics-metadata/page-scanner-utils.ts b/src/internal/analytics-metadata/page-scanner-utils.ts
index 88d5f21..ff93e7b 100644
--- a/src/internal/analytics-metadata/page-scanner-utils.ts
+++ b/src/internal/analytics-metadata/page-scanner-utils.ts
@@ -8,6 +8,7 @@ import { getGeneratedAnalyticsMetadata } from './utils.js';
interface GeneratedAnalyticsMetadataComponentTree {
name: string;
label: string;
+ description?: string;
properties?: Record | Array> | Array | Array>;
children?: Array;
}