diff --git a/src/internal/analytics-metadata/__tests__/dom-utils.test.tsx b/src/internal/analytics-metadata/__tests__/dom-utils.test.tsx
index 05bb1d4..d77520f 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, findComponentUpUntil, findSelectorUp } from '../dom-utils';
beforeAll(() => {
activateAnalyticsMetadata(true);
@@ -78,9 +78,9 @@ describe('isNodeComponent', () => {
});
});
-describe('findComponentUp', () => {
+describe('findComponentUpUntil', () => {
test('returns null when input is null', () => {
- expect(findComponentUp(null)).toBeNull();
+ expect(findComponentUpUntil(null)).toBeNull();
});
test('returns parent component element', () => {
const { container } = render(
@@ -88,7 +88,7 @@ describe('findComponentUp', () => {
);
- expect(findComponentUp(container.querySelector('#target-element'))!.id).toBe('component-element');
+ expect(findComponentUpUntil(container.querySelector('#target-element'))!.id).toBe('component-element');
});
test('returns parent component element with portals', () => {
const { container } = render(
@@ -101,7 +101,7 @@ describe('findComponentUp', () => {
);
- expect(findComponentUp(container.querySelector('#target-element'))!.id).toBe('component-element');
+ expect(findComponentUpUntil(container.querySelector('#target-element'))!.id).toBe('component-element');
});
test('returns null when element has no parent component', () => {
const { container } = render(
@@ -109,7 +109,54 @@ describe('findComponentUp', () => {
);
- expect(findComponentUp(container.querySelector('#target-element'))).toBeNull();
+ expect(findComponentUpUntil(container.querySelector('#target-element'))).toBeNull();
+ });
+ test('with `until` argument', () => {
+ const { container } = render(
+ <>
+
+
+ >
+ );
+ expect(
+ findComponentUpUntil(
+ container.querySelector('#target-element'),
+ container.querySelector('#outer-element') as HTMLElement
+ )!.id
+ ).toBe('component-element');
+ expect(
+ findComponentUpUntil(
+ container.querySelector('#target-element'),
+ container.querySelector('#target-element') as HTMLElement
+ )
+ ).toBeNull();
+ expect(
+ findComponentUpUntil(
+ container.querySelector('#target-element'),
+ container.querySelector('#component-element') as HTMLElement
+ )!.id
+ ).toBe('component-element');
+ expect(
+ findComponentUpUntil(
+ container.querySelector('#another-target-element'),
+ container.querySelector('#outer-element') as HTMLElement
+ )!.id
+ ).toBe('component-element');
+ expect(
+ findComponentUpUntil(
+ container.querySelector('#another-target-element'),
+ container.querySelector('#another-until') as HTMLElement
+ )
+ ).toBeNull();
});
});
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..2661620 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', () => {
@@ -55,8 +55,11 @@ describe('getComponentsTree', () => {
{
name: 'ComponentThree',
children: [
- { name: 'ComponentTwo', label: 'sub label' },
- { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
+ {
+ name: 'ComponentTwo',
+ label: 'sub label',
+ children: [{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } }],
+ },
],
},
{ name: 'ComponentTwo', label: 'sub label' },
@@ -66,7 +69,7 @@ describe('getComponentsTree', () => {
const { container } = render(
-
+
);
@@ -74,15 +77,12 @@ describe('getComponentsTree', () => {
{
name: 'ComponentThree',
children: [
- { name: 'ComponentTwo', label: 'sub label' },
- { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
{
- name: 'ComponentThree',
- children: [
- { name: 'ComponentTwo', label: 'sub label' },
- { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
- ],
+ name: 'ComponentTwo',
+ label: 'sub label',
+ children: [{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } }],
},
+ { name: 'ComponentTwo', label: 'sub label' },
],
},
]);
@@ -98,8 +98,11 @@ describe('getComponentsTree', () => {
{
name: 'ComponentThree',
children: [
- { name: 'ComponentTwo', label: 'sub label' },
- { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
+ {
+ name: 'ComponentTwo',
+ label: 'sub label',
+ children: [{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } }],
+ },
],
},
{ name: 'ComponentTwo', label: 'sub label' },
@@ -116,6 +119,83 @@ 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: 'ComponentFour',
+ children: [
+ { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
+ { 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..1a79b55 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_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}"]`);
}
@@ -15,12 +15,12 @@ export const findLogicalParent = (node: HTMLElement): HTMLElement | null => {
}
};
-export function findComponentUp(node: HTMLElement | null): HTMLElement | null {
+export function findComponentUpUntil(node: HTMLElement | null, until: HTMLElement = document.body): HTMLElement | null {
let firstComponentElement = node;
- while (firstComponentElement && firstComponentElement.tagName !== 'body' && !isNodeComponent(firstComponentElement)) {
+ while (firstComponentElement && firstComponentElement !== until && !isNodeComponent(firstComponentElement)) {
firstComponentElement = findLogicalParent(firstComponentElement);
}
- return firstComponentElement && firstComponentElement.tagName !== 'body' ? firstComponentElement : null;
+ return firstComponentElement && isNodeComponent(firstComponentElement) ? firstComponentElement : null;
}
export const isNodeComponent = (node: HTMLElement): boolean => {
diff --git a/src/internal/analytics-metadata/labels-utils.ts b/src/internal/analytics-metadata/labels-utils.ts
index 1ac173e..0ef2e5b 100644
--- a/src/internal/analytics-metadata/labels-utils.ts
+++ b/src/internal/analytics-metadata/labels-utils.ts
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { LABEL_DATA_ATTRIBUTE } from './attributes';
-import { findSelectorUp, findComponentUp } from './dom-utils';
+import { findSelectorUp, findComponentUpUntil } from './dom-utils';
import { LabelIdentifier } from './interfaces';
export const processLabel = (node: HTMLElement | null, labelIdentifier: string | LabelIdentifier | null): string => {
@@ -52,7 +52,7 @@ const processSingleLabel = (
return processSingleLabel(findSelectorUp(node, rootSelector), labelSelector);
}
if (root === 'component') {
- return processSingleLabel(findComponentUp(node), labelSelector);
+ return processSingleLabel(findComponentUpUntil(node), labelSelector);
}
if (root === 'body') {
return processSingleLabel(document.body, labelSelector);
diff --git a/src/internal/analytics-metadata/page-scanner-utils.ts b/src/internal/analytics-metadata/page-scanner-utils.ts
index e90f645..ece9bf8 100644
--- a/src/internal/analytics-metadata/page-scanner-utils.ts
+++ b/src/internal/analytics-metadata/page-scanner-utils.ts
@@ -1,8 +1,8 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import { METADATA_ATTRIBUTE } from './attributes';
-import { isNodeComponent } from './dom-utils';
+import { METADATA_ATTRIBUTE, REFERRER_ATTRIBUTE, REFERRER_DATA_ATTRIBUTE } from './attributes';
+import { findComponentUpUntil, isNodeComponent } from './dom-utils';
import { getGeneratedAnalyticsMetadata } from './utils';
interface GeneratedAnalyticsMetadataComponentTree {
@@ -12,26 +12,57 @@ interface GeneratedAnalyticsMetadataComponentTree {
children?: Array;
}
+interface ComponentsMap {
+ roots: Array;
+ parents: Map>;
+}
+
+const findPortalsOutsideOfNode = (node: HTMLElement): Array =>
+ (Array.from(document.querySelectorAll(`[${REFERRER_ATTRIBUTE}]`)) as Array).filter(element => {
+ const referrer = element.dataset[REFERRER_DATA_ATTRIBUTE];
+ return !!node.querySelector(`[id="${referrer}"]`) && !node.querySelector(`[${REFERRER_ATTRIBUTE}="${referrer}"]`);
+ });
+
const getComponentsArray = (node: HTMLElement | Document = document) => {
const elementsWithMetadata = Array.from(node.querySelectorAll(`[${METADATA_ATTRIBUTE}]`)) as Array;
+ findPortalsOutsideOfNode(node as HTMLElement).forEach(portal => {
+ elementsWithMetadata.push(...getComponentsArray(portal));
+ });
return elementsWithMetadata.filter(isNodeComponent);
};
+const buildComponentsMap = (node: HTMLElement | Document = document) => {
+ const componentsArray = getComponentsArray(node);
+ const map: ComponentsMap = {
+ roots: [],
+ parents: new Map>(),
+ };
+ componentsArray.forEach(element => {
+ const parent = element.parentElement ? findComponentUpUntil(element.parentElement, node as HTMLElement) : null;
+ if (!parent) {
+ map.roots.push(element);
+ } else {
+ if (!map.parents.has(parent)) {
+ map.parents.set(parent, []);
+ }
+ map.parents.get(parent)?.push(element);
+ }
+ });
+ return map;
+};
+
const getComponentsTreeRecursive = (
- node: HTMLElement | Document,
- visited: Set
+ componentNodes: Array,
+ parentsMap: Map>
): Array => {
const tree: Array = [];
- const componentNodes = getComponentsArray(node);
componentNodes.forEach(componentNode => {
- if (visited.has(componentNode)) {
- return;
- }
- visited.add(componentNode);
const treeItem: GeneratedAnalyticsMetadataComponentTree = {
...getGeneratedAnalyticsMetadata(componentNode).contexts[0].detail,
};
- const children = getComponentsTreeRecursive(componentNode, visited);
+ const children = parentsMap.has(componentNode)
+ ? getComponentsTreeRecursive(parentsMap.get(componentNode)!, parentsMap)
+ : [];
if (children.length > 0) {
treeItem.children = children;
}
@@ -46,5 +77,6 @@ export const getComponentsTree = (
if (!node) {
return [];
}
- return getComponentsTreeRecursive(node, new Set());
+ const { roots, parents } = buildComponentsMap(node);
+ return getComponentsTreeRecursive(roots, parents);
};