Skip to content

Commit eb48d66

Browse files
authored
fix: Support icons for button dropdown groups (#3710)
1 parent 1250d57 commit eb48d66

File tree

7 files changed

+216
-4
lines changed

7 files changed

+216
-4
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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
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

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

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

Lines changed: 9 additions & 1 deletion
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,7 +32,14 @@ const CategoryElement = ({
3132
>
3233
{item.text && (
3334
<p className={clsx(styles.header, { [styles.disabled]: disabled })} aria-hidden="true">
34-
{item.text}
35+
<span className={styles['header-content']}>
36+
{(item.iconName || item.iconUrl || item.iconSvg) && (
37+
<span className={styles['icon-wrapper']}>
38+
<InternalIcon name={item.iconName} url={item.iconUrl} svg={item.iconSvg} alt={item.iconAlt} />
39+
</span>
40+
)}
41+
{item.text}
42+
</span>
3543
</p>
3644
)}
3745
<ul className={styles['items-list-container']} role="group" aria-label={item.text} aria-disabled={disabled}>

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

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

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

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

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
font-weight: bold;
1919
display: flex;
2020
justify-content: space-between;
21+
align-items: center;
2122
// to compensate for the loss of padding due to the removal of the left and right borders
2223
// and differences in default divider + selected border widths (visual refresh)
2324
padding-block: styles.$option-padding-with-border-placeholder-vertical;
@@ -33,6 +34,7 @@
3334
border-block-start-color: awsui.$color-border-dropdown-group;
3435
border-block-end-color: awsui.$color-border-dropdown-group;
3536
cursor: pointer;
37+
3638
&.disabled {
3739
cursor: default;
3840
}
@@ -99,9 +101,10 @@
99101

100102
.expand-icon {
101103
position: relative;
102-
inset-inline-start: awsui.$space-s;
104+
inset-inline-end: calc(-1 * #{awsui.$space-s});
103105
inline-size: awsui.$space-m;
104106
display: inline-block;
107+
margin-inline-start: auto;
105108

106109
@include styles.with-motion {
107110
transition: transform awsui.$motion-duration-rotate-180 awsui.$motion-easing-rotate-180;
@@ -113,7 +116,6 @@
113116

114117
&-right {
115118
transform: rotate(-90deg);
116-
117119
@include styles.with-direction('rtl') {
118120
transform: rotate(90deg);
119121
}
@@ -132,3 +134,12 @@
132134
.in-dropdown {
133135
margin-block-end: -1px;
134136
}
137+
138+
.icon-wrapper {
139+
margin-inline-end: awsui.$space-xxs;
140+
}
141+
142+
.header-content {
143+
display: flex;
144+
align-items: center;
145+
}

0 commit comments

Comments
 (0)