diff --git a/src/internal/analytics-metadata/__tests__/dom-utils.test.tsx b/src/internal/analytics-metadata/__tests__/dom-utils.test.tsx
index 05bb1d4..9691e33 100644
--- a/src/internal/analytics-metadata/__tests__/dom-utils.test.tsx
+++ b/src/internal/analytics-metadata/__tests__/dom-utils.test.tsx
@@ -4,7 +4,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { activateAnalyticsMetadata, getAnalyticsMetadataAttribute, METADATA_ATTRIBUTE } from '../attributes';
-import { findLogicalParent, isNodeComponent, findComponentUp, findSelectorUp } from '../dom-utils';
+import { findLogicalParent, isNodeComponent, findComponentUp, findSelectorUp, findPortals } from '../dom-utils';
beforeAll(() => {
activateAnalyticsMetadata(true);
@@ -153,3 +153,59 @@ describe('findSelectorUp', () => {
expect(findSelectorUp(container.querySelector('#target-element'), '.test-class')).toBeNull();
});
});
+
+describe('findPortals', () => {
+ test('returns an empty array when no referrerId is found', () => {
+ const { container } = render(
);
+ expect(findPortals(container.querySelector('#root-element')!)).toEqual([]);
+ });
+ test('returns an empty array when no portal is found', () => {
+ const { container } = render(
+
+ );
+ expect(findPortals(container.querySelector('#root-element')!)).toEqual([]);
+ });
+ test('returns one portal', () => {
+ const { container } = render(
+
+ );
+ expect(findPortals(container.querySelector('#root-element')!)).toEqual([container.querySelector('.portal')]);
+ });
+ test('returns multiple portals', () => {
+ const { container } = render(
+ <>
+
+
+
+ >
+ );
+ expect(findPortals(container.querySelector('#root-element')!)).toEqual(
+ [1, 2, 3].map(index => container.querySelector(`.portal-${index}`))
+ );
+ });
+ test('returns only portals contained in the specified elements', () => {
+ const { container } = render(
+
+ );
+ expect(findPortals(container.querySelector('#target-element')!)).toEqual([container.querySelector('.portal-2')]);
+ });
+});
diff --git a/src/internal/analytics-metadata/__tests__/page-scanner-utils.test.tsx b/src/internal/analytics-metadata/__tests__/page-scanner-utils.test.tsx
index d6998e9..24a5b62 100644
--- a/src/internal/analytics-metadata/__tests__/page-scanner-utils.test.tsx
+++ b/src/internal/analytics-metadata/__tests__/page-scanner-utils.test.tsx
@@ -4,7 +4,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { getComponentsTree } from '../utils';
-import { METADATA_ATTRIBUTE, activateAnalyticsMetadata } from '../attributes';
+import { METADATA_ATTRIBUTE, activateAnalyticsMetadata, getAnalyticsMetadataAttribute } from '../attributes';
import { ComponentOne, ComponentTwo, ComponentThree } from './components';
describe('getComponentsTree', () => {
@@ -116,6 +116,81 @@ describe('getComponentsTree', () => {
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
]);
});
+ describe('with portals', () => {
+ test('returns an empty array when portal outside of the node element', () => {
+ const { container } = render(
+
+ );
+ const target = container.querySelector('#inner-target') as HTMLElement;
+ expect(getComponentsTree(target)).toEqual([]);
+ });
+ test('returns nested portal correctly', () => {
+ const { container } = render(
+
+ );
+ expect(getComponentsTree(container.querySelector('#outer-target') as HTMLElement)).toEqual([
+ { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
+ {
+ name: 'ComponentFour',
+ children: [{ name: 'ComponentTwo', label: 'sub label' }],
+ },
+ ]);
+ expect(getComponentsTree(container.querySelector('#inner-target') as HTMLElement)).toEqual([
+ {
+ name: 'ComponentFour',
+ children: [
+ { name: 'ComponentTwo', label: 'sub label' },
+ { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
+ ],
+ },
+ ]);
+ });
+ test('returns recursively nested portals', () => {
+ const { container } = render(
+
+ );
+ expect(getComponentsTree(container.querySelector('#inner-target') as HTMLElement)).toEqual([
+ {
+ name: 'ComponentFour',
+ children: [
+ { name: 'ComponentTwo', label: 'sub label' },
+ { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
+ { name: 'ComponentFive' },
+ ],
+ },
+ ]);
+ });
+ });
});
describe('with inactive analytics metadata', () => {
diff --git a/src/internal/analytics-metadata/attributes.ts b/src/internal/analytics-metadata/attributes.ts
index f9715ca..f9ee324 100644
--- a/src/internal/analytics-metadata/attributes.ts
+++ b/src/internal/analytics-metadata/attributes.ts
@@ -7,6 +7,8 @@ import { getGlobalFlag } from '../global-flags';
export const METADATA_DATA_ATTRIBUTE = 'awsuiAnalytics';
export const METADATA_ATTRIBUTE = 'data-awsui-analytics';
export const LABEL_DATA_ATTRIBUTE = 'awsuiAnalyticsLabel';
+export const REFERRER_DATA_ATTRIBUTE = 'awsuiReferrerId';
+export const REFERRER_ATTRIBUTE = 'data-awsui-referrer-id';
const LABEL_ATTRIBUTE = 'data-awsui-analytics-label';
let activated = getGlobalFlag('analyticsMetadata');
diff --git a/src/internal/analytics-metadata/dom-utils.ts b/src/internal/analytics-metadata/dom-utils.ts
index 6cfd160..9d278f5 100644
--- a/src/internal/analytics-metadata/dom-utils.ts
+++ b/src/internal/analytics-metadata/dom-utils.ts
@@ -1,11 +1,11 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import { METADATA_DATA_ATTRIBUTE } from './attributes';
+import { METADATA_DATA_ATTRIBUTE, REFERRER_ATTRIBUTE, REFERRER_DATA_ATTRIBUTE } from './attributes';
export const findLogicalParent = (node: HTMLElement): HTMLElement | null => {
try {
- const referrer = node.dataset.awsuiReferrerId;
+ const referrer = node.dataset[REFERRER_DATA_ATTRIBUTE];
if (referrer) {
return document.querySelector(`[id="${referrer}"]`);
}
@@ -43,3 +43,8 @@ export function findSelectorUp(node: HTMLElement | null, selector: string): HTML
}
return current && current.tagName !== 'body' ? current : null;
}
+
+export const findPortals = (node: HTMLElement): Array =>
+ Array.from(node.querySelectorAll(`[${REFERRER_ATTRIBUTE}]`))
+ .map(element => findLogicalParent(element as HTMLElement))
+ .filter(element => !!element) as Array;
diff --git a/src/internal/analytics-metadata/page-scanner-utils.ts b/src/internal/analytics-metadata/page-scanner-utils.ts
index e90f645..e084eb2 100644
--- a/src/internal/analytics-metadata/page-scanner-utils.ts
+++ b/src/internal/analytics-metadata/page-scanner-utils.ts
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { METADATA_ATTRIBUTE } from './attributes';
-import { isNodeComponent } from './dom-utils';
+import { findPortals, isNodeComponent } from './dom-utils';
import { getGeneratedAnalyticsMetadata } from './utils';
interface GeneratedAnalyticsMetadataComponentTree {
@@ -14,6 +14,9 @@ interface GeneratedAnalyticsMetadataComponentTree {
const getComponentsArray = (node: HTMLElement | Document = document) => {
const elementsWithMetadata = Array.from(node.querySelectorAll(`[${METADATA_ATTRIBUTE}]`)) as Array;
+ findPortals(node as HTMLElement).forEach(portal => {
+ elementsWithMetadata.push(...getComponentsArray(portal));
+ });
return elementsWithMetadata.filter(isNodeComponent);
};