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); };