Skip to content

Commit ec52000

Browse files
author
saday giridhar
committed
Address PR review comments
- Move getDataAttributes to reusable internal/utils location - Use warnOnce utility instead of console.warn - Move dataAttributes documentation to items prop description - Rewrite tests to use ButtonDropdown test-utils - Combine multiple test cases into single comprehensive test - Add test for data- prefix not being duplicated - Fix TypeScript and linting issues
1 parent 7d5ba75 commit ec52000

File tree

4 files changed

+75
-104
lines changed

4 files changed

+75
-104
lines changed

src/button-dropdown/__tests__/data-attributes.test.tsx

Lines changed: 42 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
// SPDX-License-Identifier: Apache-2.0
33
import React from 'react';
44
import { render } from '@testing-library/react';
5+
56
import ButtonDropdown from '../../../lib/components/button-dropdown';
7+
import createWrapper from '../../../lib/components/test-utils/dom';
68

79
describe('ButtonDropdown data attributes', () => {
810
test('renders custom data attributes on items', () => {
9-
const { getByText } = render(
11+
const { container } = render(
1012
<ButtonDropdown
1113
items={[
1214
{
@@ -21,32 +23,37 @@ describe('ButtonDropdown data attributes', () => {
2123
/>
2224
);
2325

24-
const item = getByText('Edit').closest('li');
26+
const wrapper = createWrapper(container).findButtonDropdown()!;
27+
wrapper.openDropdown();
28+
const item = wrapper.findItemById('edit')!.getElement();
29+
2530
expect(item).toHaveAttribute('data-analytics-action', 'edit-product');
2631
expect(item).toHaveAttribute('data-item-key', 'product-123');
2732
});
2833

29-
test('automatically prepends data- prefix', () => {
30-
const { getByText } = render(
34+
test('does not duplicate data- prefix when already present', () => {
35+
const { container } = render(
3136
<ButtonDropdown
3237
items={[
3338
{
34-
id: 'delete',
35-
text: 'Delete',
36-
dataAttributes: { 'custom-attr': 'value' },
39+
id: 'action',
40+
text: 'Action',
41+
dataAttributes: { 'data-custom': 'value' },
3742
},
3843
]}
3944
/>
4045
);
4146

42-
const item = getByText('Delete').closest('li');
43-
expect(item).toHaveAttribute('data-custom-attr', 'value');
47+
const wrapper = createWrapper(container).findButtonDropdown()!;
48+
wrapper.openDropdown();
49+
const item = wrapper.findItemById('action')!.getElement();
50+
51+
expect(item).toHaveAttribute('data-custom', 'value');
52+
expect(item).not.toHaveAttribute('data-data-custom');
4453
});
4554

4655
test('excludes testid from dataAttributes', () => {
47-
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
48-
49-
const { getByText } = render(
56+
const { container } = render(
5057
<ButtonDropdown
5158
items={[
5259
{
@@ -58,80 +65,54 @@ describe('ButtonDropdown data attributes', () => {
5865
/>
5966
);
6067

61-
const item = getByText('Action').closest('li');
68+
const wrapper = createWrapper(container).findButtonDropdown()!;
69+
wrapper.openDropdown();
70+
const item = wrapper.findItemById('original-id')!.getElement();
71+
6272
expect(item).toHaveAttribute('data-testid', 'original-id');
63-
expect(consoleSpy).toHaveBeenCalledWith(
64-
'ButtonDropdown: "testid" key is reserved and cannot be overridden via dataAttributes'
65-
);
66-
67-
consoleSpy.mockRestore();
6873
});
6974

7075
test('handles undefined values', () => {
71-
const { getByText } = render(
76+
const dataAttrs: Record<string, string | undefined> = {
77+
defined: 'value',
78+
undefined: undefined,
79+
};
80+
81+
const { container } = render(
7282
<ButtonDropdown
7383
items={[
7484
{
7585
id: 'action',
7686
text: 'Action',
77-
dataAttributes: {
78-
defined: 'value',
79-
undefined: undefined,
80-
},
87+
dataAttributes: dataAttrs as Record<string, string>,
8188
},
8289
]}
8390
/>
8491
);
8592

86-
const item = getByText('Action').closest('li');
93+
const wrapper = createWrapper(container).findButtonDropdown()!;
94+
wrapper.openDropdown();
95+
const item = wrapper.findItemById('action')!.getElement();
96+
8797
expect(item).toHaveAttribute('data-defined', 'value');
8898
expect(item).not.toHaveAttribute('data-undefined');
8999
});
90100

91-
test('works with multiple items', () => {
92-
const { getByText } = render(
101+
test('works with multiple items, disabled items, and checkbox items', () => {
102+
const { container } = render(
93103
<ButtonDropdown
94104
items={[
95105
{
96106
id: 'edit',
97107
text: 'Edit',
98108
dataAttributes: { action: 'edit' },
99109
},
100-
{
101-
id: 'delete',
102-
text: 'Delete',
103-
dataAttributes: { action: 'delete' },
104-
},
105-
]}
106-
/>
107-
);
108-
109-
expect(getByText('Edit').closest('li')).toHaveAttribute('data-action', 'edit');
110-
expect(getByText('Delete').closest('li')).toHaveAttribute('data-action', 'delete');
111-
});
112-
113-
test('works with disabled items', () => {
114-
const { getByText } = render(
115-
<ButtonDropdown
116-
items={[
117110
{
118111
id: 'disabled',
119112
text: 'Disabled',
120113
disabled: true,
121114
dataAttributes: { state: 'disabled' },
122115
},
123-
]}
124-
/>
125-
);
126-
127-
const item = getByText('Disabled').closest('li');
128-
expect(item).toHaveAttribute('data-state', 'disabled');
129-
});
130-
131-
test('works with checkbox items', () => {
132-
const { getByText } = render(
133-
<ButtonDropdown
134-
items={[
135116
{
136117
itemType: 'checkbox',
137118
id: 'checkbox-item',
@@ -143,7 +124,11 @@ describe('ButtonDropdown data attributes', () => {
143124
/>
144125
);
145126

146-
const item = getByText('Checkbox').closest('li');
147-
expect(item).toHaveAttribute('data-checkbox-id', 'cb-1');
127+
const wrapper = createWrapper(container).findButtonDropdown()!;
128+
wrapper.openDropdown();
129+
130+
expect(wrapper.findItemById('edit')!.getElement()).toHaveAttribute('data-action', 'edit');
131+
expect(wrapper.findItemById('disabled')!.getElement()).toHaveAttribute('data-state', 'disabled');
132+
expect(wrapper.findItemById('checkbox-item')!.getElement()).toHaveAttribute('data-checkbox-id', 'cb-1');
148133
});
149134
});

src/button-dropdown/interfaces.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface ButtonDropdownProps extends BaseComponentProps, ExpandToViewpor
2929
* - `disabledReason` (string) - (Optional) Displays text near the `text` property when item is disabled. Use to provide additional context.
3030
* - `description` (string) - additional data that will be passed to a `data-description` attribute. **Deprecated**, has no effect.
3131
* - `ariaLabel` (string) - (Optional) - ARIA label of the item element.
32+
* - `dataAttributes` (Record<string, string>) - (Optional) Custom data attributes for the item element. Attribute names are automatically prefixed with "data-". The "testid" key is reserved.
3233
*
3334
* ### action
3435
*
@@ -282,26 +283,6 @@ export namespace ButtonDropdownProps {
282283
iconUrl?: string;
283284
iconSvg?: React.ReactNode;
284285
labelTag?: string;
285-
/**
286-
* Custom data attributes to add to the dropdown item element.
287-
* Attribute names will automatically be prefixed with "data-".
288-
* The "testid" key is reserved and cannot be overridden.
289-
*
290-
* Use this for analytics tracking, testing selectors, or other metadata.
291-
*
292-
* @example
293-
* items={[
294-
* {
295-
* id: 'edit',
296-
* text: 'Edit',
297-
* dataAttributes: {
298-
* 'analytics-action': 'edit-product',
299-
* 'item-key': 'product-123'
300-
* }
301-
* }
302-
* ]}
303-
* // Renders as: data-analytics-action="edit-product" data-item-key="product-123"
304-
*/
305286
dataAttributes?: Record<string, string>;
306287
}
307288

src/button-dropdown/item-element/index.tsx

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import InternalIcon, { InternalIconProps } from '../../icon/internal';
1212
import { useDropdownContext } from '../../internal/components/dropdown/context';
1313
import useHiddenDescription from '../../internal/hooks/use-hidden-description';
1414
import { useVisualRefresh } from '../../internal/hooks/use-visual-mode';
15+
import { getDataAttributes } from '../../internal/utils/data-attributes';
1516
import { GeneratedAnalyticsMetadataButtonDropdownClick } from '../analytics-metadata/interfaces';
1617
import { ButtonDropdownProps, InternalCheckboxItem, InternalItem, ItemProps, LinkItem } from '../interfaces';
1718
import Tooltip from '../tooltip';
@@ -22,32 +23,6 @@ import { getItemTarget } from '../utils/utils';
2223
import analyticsLabels from '../analytics-metadata/styles.css.js';
2324
import styles from './styles.css.js';
2425

25-
/**
26-
* Converts dataAttributes object to DOM data-* attributes.
27-
* - Automatically prepends 'data-' prefix if not present
28-
* - Excludes 'testid' to prevent overriding existing behavior
29-
* - Filters out undefined values
30-
*/
31-
const getDataAttributes = (dataAttributes?: Record<string, string>): Record<string, string> => {
32-
if (!dataAttributes) return {};
33-
34-
return Object.entries(dataAttributes).reduce((acc, [key, value]) => {
35-
// Exclude 'testid' to prevent overriding existing data-testid behavior
36-
if (key === 'testid' || key === 'data-testid') {
37-
console.warn('ButtonDropdown: "testid" key is reserved and cannot be overridden via dataAttributes');
38-
return acc;
39-
}
40-
41-
// Skip undefined values
42-
if (value === undefined) return acc;
43-
44-
// Add 'data-' prefix if not already present
45-
const attrKey = key.startsWith('data-') ? key : `data-${key}`;
46-
acc[attrKey] = value;
47-
return acc;
48-
}, {} as Record<string, string>);
49-
};
50-
5126
const ItemElement = ({
5227
position = '1',
5328
index,
@@ -99,7 +74,7 @@ const ItemElement = ({
9974
role="presentation"
10075
data-testid={item.id}
10176
data-description={item.description}
102-
{...getDataAttributes(item.dataAttributes)}
77+
{...getDataAttributes(item.dataAttributes, ['testid'])}
10378
onClick={onClick}
10479
onMouseEnter={onHover}
10580
onTouchStart={onHover}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { warnOnce } from '@cloudscape-design/component-toolkit/internal';
4+
5+
export function getDataAttributes(
6+
dataAttributes: Record<string, string> | undefined,
7+
excludeKeys: string[] = []
8+
): Record<string, string> {
9+
if (!dataAttributes) {
10+
return {};
11+
}
12+
13+
return Object.entries(dataAttributes).reduce(
14+
(acc, [key, value]) => {
15+
if (excludeKeys.includes(key) || excludeKeys.includes(`data-${key}`)) {
16+
warnOnce('getDataAttributes', `"${key}" key is reserved and cannot be overridden via dataAttributes`);
17+
return acc;
18+
}
19+
20+
if (value === undefined) {
21+
return acc;
22+
}
23+
24+
const attrKey = key.startsWith('data-') ? key : `data-${key}`;
25+
acc[attrKey] = value;
26+
return acc;
27+
},
28+
{} as Record<string, string>
29+
);
30+
}

0 commit comments

Comments
 (0)