diff --git a/src/button-dropdown/__tests__/data-attributes.test.tsx b/src/button-dropdown/__tests__/data-attributes.test.tsx new file mode 100644 index 0000000000..2b815bbc44 --- /dev/null +++ b/src/button-dropdown/__tests__/data-attributes.test.tsx @@ -0,0 +1,149 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import { render } from '@testing-library/react'; +import ButtonDropdown from '../../../lib/components/button-dropdown'; + +describe('ButtonDropdown data attributes', () => { + test('renders custom data attributes on items', () => { + const { getByText } = render( + + ); + + const item = getByText('Edit').closest('li'); + expect(item).toHaveAttribute('data-analytics-action', 'edit-product'); + expect(item).toHaveAttribute('data-item-key', 'product-123'); + }); + + test('automatically prepends data- prefix', () => { + const { getByText } = render( + + ); + + const item = getByText('Delete').closest('li'); + expect(item).toHaveAttribute('data-custom-attr', 'value'); + }); + + test('excludes testid from dataAttributes', () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + const { getByText } = render( + + ); + + const item = getByText('Action').closest('li'); + expect(item).toHaveAttribute('data-testid', 'original-id'); + expect(consoleSpy).toHaveBeenCalledWith( + 'ButtonDropdown: "testid" key is reserved and cannot be overridden via dataAttributes' + ); + + consoleSpy.mockRestore(); + }); + + test('handles undefined values', () => { + const { getByText } = render( + + ); + + const item = getByText('Action').closest('li'); + expect(item).toHaveAttribute('data-defined', 'value'); + expect(item).not.toHaveAttribute('data-undefined'); + }); + + test('works with multiple items', () => { + const { getByText } = render( + + ); + + expect(getByText('Edit').closest('li')).toHaveAttribute('data-action', 'edit'); + expect(getByText('Delete').closest('li')).toHaveAttribute('data-action', 'delete'); + }); + + test('works with disabled items', () => { + const { getByText } = render( + + ); + + const item = getByText('Disabled').closest('li'); + expect(item).toHaveAttribute('data-state', 'disabled'); + }); + + test('works with checkbox items', () => { + const { getByText } = render( + + ); + + const item = getByText('Checkbox').closest('li'); + expect(item).toHaveAttribute('data-checkbox-id', 'cb-1'); + }); +}); diff --git a/src/button-dropdown/interfaces.ts b/src/button-dropdown/interfaces.ts index 95d93c06ac..2e70536a21 100644 --- a/src/button-dropdown/interfaces.ts +++ b/src/button-dropdown/interfaces.ts @@ -282,6 +282,27 @@ export namespace ButtonDropdownProps { iconUrl?: string; iconSvg?: React.ReactNode; labelTag?: string; + /** + * Custom data attributes to add to the dropdown item element. + * Attribute names will automatically be prefixed with "data-". + * The "testid" key is reserved and cannot be overridden. + * + * Use this for analytics tracking, testing selectors, or other metadata. + * + * @example + * items={[ + * { + * id: 'edit', + * text: 'Edit', + * dataAttributes: { + * 'analytics-action': 'edit-product', + * 'item-key': 'product-123' + * } + * } + * ]} + * // Renders as: data-analytics-action="edit-product" data-item-key="product-123" + */ + dataAttributes?: Record; } export interface CheckboxItem diff --git a/src/button-dropdown/item-element/index.tsx b/src/button-dropdown/item-element/index.tsx index 5ee7e11f3a..af68c352aa 100644 --- a/src/button-dropdown/item-element/index.tsx +++ b/src/button-dropdown/item-element/index.tsx @@ -22,6 +22,32 @@ import { getItemTarget } from '../utils/utils'; import analyticsLabels from '../analytics-metadata/styles.css.js'; import styles from './styles.css.js'; +/** + * Converts dataAttributes object to DOM data-* attributes. + * - Automatically prepends 'data-' prefix if not present + * - Excludes 'testid' to prevent overriding existing behavior + * - Filters out undefined values + */ +const getDataAttributes = (dataAttributes?: Record): Record => { + if (!dataAttributes) return {}; + + return Object.entries(dataAttributes).reduce((acc, [key, value]) => { + // Exclude 'testid' to prevent overriding existing data-testid behavior + if (key === 'testid' || key === 'data-testid') { + console.warn('ButtonDropdown: "testid" key is reserved and cannot be overridden via dataAttributes'); + return acc; + } + + // Skip undefined values + if (value === undefined) return acc; + + // Add 'data-' prefix if not already present + const attrKey = key.startsWith('data-') ? key : `data-${key}`; + acc[attrKey] = value; + return acc; + }, {} as Record); +}; + const ItemElement = ({ position = '1', index, @@ -73,6 +99,7 @@ const ItemElement = ({ role="presentation" data-testid={item.id} data-description={item.description} + {...getDataAttributes(item.dataAttributes)} onClick={onClick} onMouseEnter={onHover} onTouchStart={onHover}