diff --git a/src/internal/analytics-metadata/__tests__/components.tsx b/src/internal/analytics-metadata/__tests__/components.tsx index 1e09ca2..2005400 100644 --- a/src/internal/analytics-metadata/__tests__/components.tsx +++ b/src/internal/analytics-metadata/__tests__/components.tsx @@ -52,6 +52,7 @@ export const ComponentThree = () => ( position: '2', columnLabel: { selector: '.invalid-selector', root: 'self' }, anotherLabel: { root: 'self' }, + yetAnotherLabel: { rootSelector: '.root-class-name' }, }, }, })} diff --git a/src/internal/analytics-metadata/__tests__/dom-utils.test.tsx b/src/internal/analytics-metadata/__tests__/dom-utils.test.tsx index a66ae81..05bb1d4 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 } from '../dom-utils'; +import { findLogicalParent, isNodeComponent, findComponentUp, findSelectorUp } from '../dom-utils'; beforeAll(() => { activateAnalyticsMetadata(true); @@ -18,7 +18,7 @@ describe('findLogicalParent', () => { ); const child = container.querySelector('#child'); - expect(findLogicalParent(child as HTMLElement)?.id).toEqual('parent'); + expect(findLogicalParent(child as HTMLElement)!.id).toEqual('parent'); }); test('returns null when child does not exist', () => { const { container } = render( @@ -88,7 +88,7 @@ describe('findComponentUp', () => {
); - expect(findComponentUp(container.querySelector('#target-element'))?.id).toBe('component-element'); + expect(findComponentUp(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(findComponentUp(container.querySelector('#target-element'))!.id).toBe('component-element'); }); test('returns null when element has no parent component', () => { const { container } = render( @@ -112,3 +112,44 @@ describe('findComponentUp', () => { expect(findComponentUp(container.querySelector('#target-element'))).toBeNull(); }); }); + +describe('findSelectorUp', () => { + test('returns null when the node is null or the className is invalid', () => { + expect(findSelectorUp(null, 'abcd')).toBeNull(); + const { container } = render( +
+
+
+ ); + expect(findSelectorUp(container.querySelector('#target-element'), '.dummy')).toBeNull(); + }); + test('returns root element', () => { + const { container } = render( +
+
+
+ ); + expect(findSelectorUp(container.querySelector('#target-element'), '.test-class')!.id).toBe('root-element'); + }); + test('returns parent component element with portals', () => { + const { container } = render( +
+
+
+
+
+
+
+
+ ); + expect(findSelectorUp(container.querySelector('#target-element'), '.test-class')!.id).toBe('root-element'); + }); + test('returns null when element has no parent element with className', () => { + const { container } = render( +
+
+
+ ); + expect(findSelectorUp(container.querySelector('#target-element'), '.test-class')).toBeNull(); + }); +}); diff --git a/src/internal/analytics-metadata/__tests__/labels-utils.test.tsx b/src/internal/analytics-metadata/__tests__/labels-utils.test.tsx index fb61071..e5d2264 100644 --- a/src/internal/analytics-metadata/__tests__/labels-utils.test.tsx +++ b/src/internal/analytics-metadata/__tests__/labels-utils.test.tsx @@ -225,6 +225,43 @@ describe('processLabel', () => { expect(processLabel(target, { selector: '.outer-class', root: 'body' })).toEqual('label outside of the component'); }); + test('respects the rootSelector property', () => { + const { container } = render( +
+
outer label
+
+
inner label
+
+
+ ); + const target = container.querySelector('#target') as HTMLElement; + expect(processLabel(target, { selector: '.label-class', rootSelector: '.root-class' })).toEqual('outer label'); + }); + test('rootSelector prevails over root property', () => { + const { container } = render( + <> +
+
root class label
+
+
component label
+
+
inner label
+
+
+
+
label outside of the component
+ + ); + const target = container.querySelector('#target') as HTMLElement; + expect(processLabel(target, { selector: '.label-class', root: 'self', rootSelector: '.root-class' })).toEqual( + 'root class label' + ); + expect(processLabel(target, { selector: '.label-class', root: 'component', rootSelector: '.root-class' })).toEqual( + 'root class label' + ); + expect(processLabel(target, { selector: '.outer-class', root: 'body', rootSelector: '.root-class' })).toEqual(''); + }); + test('forwards the label resolution with data-awsui-analytics-label', () => { const { container } = render(
diff --git a/src/internal/analytics-metadata/__tests__/testing-utils.test.tsx b/src/internal/analytics-metadata/__tests__/testing-utils.test.tsx index 2a29dde..99be044 100644 --- a/src/internal/analytics-metadata/__tests__/testing-utils.test.tsx +++ b/src/internal/analytics-metadata/__tests__/testing-utils.test.tsx @@ -59,6 +59,9 @@ describe('getRawAnalyticsMetadata', () => { anotherLabel: { root: 'self', }, + yetAnotherLabel: { + rootSelector: '.root-class-name', + }, }, }, }, @@ -74,6 +77,7 @@ describe('getRawAnalyticsMetadata', () => { '.component-label', '.component-label', '.invalid-selector', + '.root-class-name', ], }); }); diff --git a/src/internal/analytics-metadata/__tests__/utils.test.tsx b/src/internal/analytics-metadata/__tests__/utils.test.tsx index 2a1ec7d..88e6946 100644 --- a/src/internal/analytics-metadata/__tests__/utils.test.tsx +++ b/src/internal/analytics-metadata/__tests__/utils.test.tsx @@ -70,6 +70,7 @@ describe('getGeneratedAnalyticsMetadata', () => { position: '2', columnLabel: '', anotherLabel: 'sub labelanother text content to ignorecontentcomponent labelevent label', + yetAnotherLabel: '', }, }, }, diff --git a/src/internal/analytics-metadata/dom-utils.ts b/src/internal/analytics-metadata/dom-utils.ts index 96010b3..6cfd160 100644 --- a/src/internal/analytics-metadata/dom-utils.ts +++ b/src/internal/analytics-metadata/dom-utils.ts @@ -35,3 +35,11 @@ export const isNodeComponent = (node: HTMLElement): boolean => { return false; } }; + +export function findSelectorUp(node: HTMLElement | null, selector: string): HTMLElement | null { + let current: HTMLElement | null = node; + while (current && current.tagName !== 'body' && !current.matches(selector)) { + current = findLogicalParent(current); + } + return current && current.tagName !== 'body' ? current : null; +} diff --git a/src/internal/analytics-metadata/interfaces.ts b/src/internal/analytics-metadata/interfaces.ts index 27e1527..383dd3c 100644 --- a/src/internal/analytics-metadata/interfaces.ts +++ b/src/internal/analytics-metadata/interfaces.ts @@ -37,6 +37,7 @@ interface GeneratedAnalyticsMetadataComponentContext { export interface LabelIdentifier { selector?: string | Array; root?: 'component' | 'self' | 'body'; + rootSelector?: string; } export interface GeneratedAnalyticsMetadataFragment extends Omit, 'detail'> { diff --git a/src/internal/analytics-metadata/labels-utils.ts b/src/internal/analytics-metadata/labels-utils.ts index a44eeea..1ac173e 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 { findComponentUp } from './dom-utils'; +import { findSelectorUp, findComponentUp } from './dom-utils'; import { LabelIdentifier } from './interfaces'; export const processLabel = (node: HTMLElement | null, labelIdentifier: string | LabelIdentifier | null): string => { @@ -13,13 +13,23 @@ export const processLabel = (node: HTMLElement | null, labelIdentifier: string | const selector = formattedLabelIdentifier.selector; if (Array.isArray(selector)) { for (const labelSelector of selector) { - const label = processSingleLabel(node, labelSelector, formattedLabelIdentifier.root); + const label = processSingleLabel( + node, + labelSelector, + formattedLabelIdentifier.root, + formattedLabelIdentifier.rootSelector + ); if (label) { return label; } } } - return processSingleLabel(node, selector as string, formattedLabelIdentifier.root); + return processSingleLabel( + node, + selector as string, + formattedLabelIdentifier.root, + formattedLabelIdentifier.rootSelector + ); }; const formatLabelIdentifier = (labelIdentifier: string | LabelIdentifier): LabelIdentifier => { @@ -32,11 +42,15 @@ const formatLabelIdentifier = (labelIdentifier: string | LabelIdentifier): Label const processSingleLabel = ( node: HTMLElement | null, labelSelector: string, - root: LabelIdentifier['root'] = 'self' + root: LabelIdentifier['root'] = 'self', + rootSelector?: string ): string => { if (!node) { return ''; } + if (rootSelector) { + return processSingleLabel(findSelectorUp(node, rootSelector), labelSelector); + } if (root === 'component') { return processSingleLabel(findComponentUp(node), labelSelector); } diff --git a/src/internal/analytics-metadata/testing-utils.ts b/src/internal/analytics-metadata/testing-utils.ts index 6e6b32d..a4033a3 100644 --- a/src/internal/analytics-metadata/testing-utils.ts +++ b/src/internal/analytics-metadata/testing-utils.ts @@ -44,13 +44,20 @@ const getLabelSelectors = (localMetadata: any): Array => { }; const getLabelSelectorsFromLabelIdentifier = (label: string | LabelIdentifier): Array => { + let labels: Array = []; if (typeof label === 'string') { - return [label]; - } else if (label.selector) { - if (typeof label.selector === 'string') { - return [label.selector]; + labels.push(label); + } else { + if (label.selector) { + if (typeof label.selector === 'string') { + labels.push(label.selector); + } else { + labels = [...label.selector]; + } + } + if (label.rootSelector) { + labels.push(label.rootSelector); } - return label.selector; } - return []; + return labels; };