Skip to content

Commit 0a89e18

Browse files
authored
feat: icons-for-expandable-buttons (#3684)
1 parent 2032136 commit 0a89e18

File tree

7 files changed

+196
-2
lines changed

7 files changed

+196
-2
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React, { useContext } from 'react';
4+
5+
import ButtonDropdown, { ButtonDropdownProps } from '~components/button-dropdown';
6+
import SpaceBetween from '~components/space-between';
7+
8+
import AppContext, { AppContextType } from '../app/app-context';
9+
import ScreenshotArea from '../utils/screenshot-area';
10+
11+
import styles from './styles.scss';
12+
13+
export const items: ButtonDropdownProps['items'] = [
14+
{
15+
id: 'category1',
16+
text: 'category1',
17+
iconName: 'gen-ai',
18+
items: [...Array(2)].map((_, index) => ({
19+
id: 'category1Subitem' + index,
20+
text: 'Sub item ' + index,
21+
})),
22+
},
23+
{
24+
id: 'category2',
25+
text: 'category2',
26+
iconUrl: 'data:image/png;base64,aaaa',
27+
items: [...Array(2)].map((_, index) => ({
28+
id: 'category2Subitem' + index,
29+
text: 'Cat 2 Sub item ' + index,
30+
})),
31+
},
32+
{
33+
id: 'item1',
34+
text: 'Item 1',
35+
iconName: 'settings',
36+
},
37+
{
38+
id: 'category3',
39+
text: 'category3',
40+
iconSvg: (
41+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" focusable="false">
42+
<path
43+
d="M2 4V12C2 12.5523 2.44772 13 3 13H13C13.5523 13 14 12.5523 14 12V6C14 5.44772 13.5523 5 13 5H8L6 3H3C2.44772 3 2 3.44772 2 4Z"
44+
fill="currentColor"
45+
/>
46+
</svg>
47+
),
48+
items: [...Array(2)].map((_, index) => ({
49+
id: 'category3Subitem' + index,
50+
text: 'Sub item ' + index,
51+
})),
52+
},
53+
];
54+
55+
type DemoContext = React.Context<
56+
AppContextType<{
57+
expandableGroups: boolean;
58+
}>
59+
>;
60+
61+
export default function IconExpandableButtonDropdown() {
62+
const {
63+
urlParams: { expandableGroups = true },
64+
setUrlParams,
65+
} = useContext(AppContext as DemoContext);
66+
67+
return (
68+
<div className={styles.container}>
69+
<article>
70+
<h1>Icon Expandable Dropdown</h1>
71+
<SpaceBetween size="m">
72+
<label>
73+
<input
74+
id="expandableGroups"
75+
type="checkbox"
76+
checked={expandableGroups}
77+
onChange={e => setUrlParams({ expandableGroups: !!e.target.checked })}
78+
/>{' '}
79+
expandableGroups
80+
</label>
81+
<ScreenshotArea
82+
style={{
83+
paddingBlockStart: 10,
84+
paddingBlockEnd: 300,
85+
paddingInlineStart: 10,
86+
paddingInlineEnd: 100,
87+
display: 'inline-block',
88+
}}
89+
>
90+
<ButtonDropdown expandableGroups={expandableGroups} items={items}>
91+
Dropdown with Icons
92+
</ButtonDropdown>
93+
</ScreenshotArea>
94+
</SpaceBetween>
95+
</article>
96+
</div>
97+
);
98+
}

src/button-dropdown/__tests__/button-dropdown-items.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ const checkRenderedGroup = (
4545
);
4646
}
4747
}
48+
49+
if (group.iconName || group.iconSvg || group.iconUrl) {
50+
expect(renderedItem.findIcon()).toBeTruthy();
51+
}
4852
};
4953

5054
const checkElementItem = (
@@ -432,6 +436,44 @@ const items: ButtonDropdownProps.Items = [
432436
wrapper.openDropdown();
433437
expect(wrapper.findItemById('i1')!.findAllIcons()).toHaveLength(2);
434438
});
439+
440+
test.each([true, false])('in category headers when expandableGroups is %s', expandableGroups => {
441+
const svg = (
442+
<svg className="test-svg" focusable="false">
443+
<circle className="test-svg-inner" cx="8" cy="8" r="7" />
444+
</svg>
445+
);
446+
const groupedCategories: ButtonDropdownProps.ItemOrGroup[] = [
447+
{
448+
id: 'category1',
449+
text: 'category1',
450+
iconName: 'folder',
451+
items: [{ id: 'i1', text: 'item1' }],
452+
},
453+
{
454+
id: 'category2',
455+
text: 'category2',
456+
iconUrl: 'data:image/png;base64,aaaa',
457+
items: [{ id: 'i2', text: 'item2' }],
458+
},
459+
{
460+
id: 'category3',
461+
text: 'category3',
462+
iconSvg: svg,
463+
items: [{ id: 'i3', text: 'item3' }],
464+
},
465+
];
466+
const wrapper = renderButtonDropdown({ ...props, expandableGroups, items: groupedCategories });
467+
wrapper.openDropdown();
468+
469+
if (expandableGroups) {
470+
expect(wrapper.findExpandableCategoryById('category1')!.findIcon()).toBeTruthy();
471+
expect(wrapper.findExpandableCategoryById('category2')!.findIcon()).toBeTruthy();
472+
expect(wrapper.findExpandableCategoryById('category3')!.findIcon()).toBeTruthy();
473+
} else {
474+
expect(wrapper.findOpenDropdown()!.findAllIcons()).toHaveLength(3);
475+
}
476+
});
435477
});
436478
});
437479
});

src/button-dropdown/__tests__/mobile-expandable-category.test.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import React from 'react';
44
import { render } from '@testing-library/react';
55

6+
import MobileExpandableCategoryElement from '../../../lib/components/button-dropdown/category-elements/mobile-expandable-category-element';
67
import MobileExpandableGroup from '../../../lib/components/button-dropdown/mobile-expandable-group/mobile-expandable-group';
78
import createWrapper from '../../../lib/components/test-utils/dom';
89

@@ -29,3 +30,46 @@ describe('MobileExpandableGroup Component', () => {
2930
expect(wrapper.find(`[data-open=true]`)).not.toBe(null);
3031
});
3132
});
33+
34+
describe('MobileExpandableCategoryElement icon rendering', () => {
35+
const mockProps = {
36+
onItemActivate: jest.fn(),
37+
onGroupToggle: jest.fn(),
38+
targetItem: null,
39+
isHighlighted: jest.fn(() => false),
40+
isKeyboardHighlight: jest.fn(() => false),
41+
isExpanded: jest.fn(() => false),
42+
lastInDropdown: false,
43+
highlightItem: jest.fn(),
44+
disabled: false,
45+
variant: 'normal' as const,
46+
position: '1',
47+
};
48+
49+
test('renders icon when iconName provided', () => {
50+
const item = { id: 'test', text: 'Test', iconName: 'settings' as const, items: [] };
51+
const wrapper = renderComponent(<MobileExpandableCategoryElement item={item} {...mockProps} />);
52+
expect(wrapper.findIcon()).toBeTruthy();
53+
});
54+
55+
test('renders icon when iconUrl provided', () => {
56+
const item = { id: 'test', text: 'Test', iconUrl: 'data:image/png;base64,test', items: [] };
57+
const wrapper = renderComponent(<MobileExpandableCategoryElement item={item} {...mockProps} />);
58+
expect(wrapper.findIcon()).toBeTruthy();
59+
});
60+
61+
test('renders icon when iconSvg provided', () => {
62+
const item = {
63+
id: 'test',
64+
text: 'Test',
65+
iconSvg: (
66+
<svg focusable="false">
67+
<circle />
68+
</svg>
69+
),
70+
items: [],
71+
};
72+
const wrapper = renderComponent(<MobileExpandableCategoryElement item={item} {...mockProps} />);
73+
expect(wrapper.findIcon()).toBeTruthy();
74+
});
75+
});

src/button-dropdown/category-elements/category-element.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import React from 'react';
44
import clsx from 'clsx';
55

6+
import InternalIcon from '../../icon/internal';
67
import { CategoryProps } from '../interfaces';
78
import ItemsList from '../items-list';
89

@@ -31,6 +32,9 @@ const CategoryElement = ({
3132
>
3233
{item.text && (
3334
<p className={clsx(styles.header, { [styles.disabled]: disabled })} aria-hidden="true">
35+
{(item.iconName || item.iconUrl || item.iconSvg) && (
36+
<InternalIcon name={item.iconName} url={item.iconUrl} svg={item.iconSvg} alt={item.iconAlt} />
37+
)}
3438
{item.text}
3539
</p>
3640
)}

src/button-dropdown/category-elements/expandable-category-element.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ const ExpandableCategoryElement = ({
9191
} as GeneratedAnalyticsMetadataButtonDropdownExpand | GeneratedAnalyticsMetadataButtonDropdownCollapse)
9292
)}
9393
>
94+
{(item.iconName || item.iconUrl || item.iconSvg) && (
95+
<InternalIcon name={item.iconName} url={item.iconUrl} svg={item.iconSvg} alt={item.iconAlt} />
96+
)}
9497
{item.text}
9598
<span className={clsx(styles['expand-icon'], styles['expand-icon-right'])}>
9699
<InternalIcon name="caret-down-filled" />

src/button-dropdown/category-elements/mobile-expandable-category-element.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ const MobileExpandableCategoryElement = ({
8383
} as GeneratedAnalyticsMetadataButtonDropdownExpand)
8484
)}
8585
>
86+
{(item.iconName || item.iconUrl || item.iconSvg) && (
87+
<InternalIcon name={item.iconName} url={item.iconUrl} svg={item.iconSvg} alt={item.iconAlt} />
88+
)}
8689
{item.text}
8790
<span
8891
className={clsx(styles['expand-icon'], {

src/button-dropdown/category-elements/styles.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
border-inline-width: 0;
1818
font-weight: bold;
1919
display: flex;
20-
justify-content: space-between;
20+
gap: awsui.$space-xxs;
2121
// to compensate for the loss of padding due to the removal of the left and right borders
2222
// and differences in default divider + selected border widths (visual refresh)
2323
padding-block: styles.$option-padding-with-border-placeholder-vertical;
@@ -113,7 +113,7 @@
113113

114114
&-right {
115115
transform: rotate(-90deg);
116-
116+
margin-inline-start: auto;
117117
@include styles.with-direction('rtl') {
118118
transform: rotate(90deg);
119119
}

0 commit comments

Comments
 (0)