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