Skip to content

Commit 5e29290

Browse files
authored
feat: Arbitrary content for select + multiselect (#4075)
1 parent 0e97d47 commit 5e29290

File tree

29 files changed

+1044
-24
lines changed

29 files changed

+1044
-24
lines changed

pages/dropdown/list-with-sticky-item.page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export default function MultiselectPage() {
9090
>
9191
<ListComponent
9292
menuProps={{ statusType: 'finished', ref, open }}
93-
getOptionProps={(option, index) => ({ option: { ...option }, key: index, open })}
93+
getOptionProps={(option, index) => ({ option: { ...option }, key: index, index: index, open })}
9494
filteredOptions={options}
9595
filteringValue={''}
9696
firstOptionSticky={true}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import * as React from 'react';
4+
5+
import { Multiselect, MultiselectProps } from '~components';
6+
import { SelectProps } from '~components/select';
7+
8+
import { SimplePage } from '../app/templates';
9+
import { i18nStrings } from './constants';
10+
const lotsOfOptions: SelectProps.Options = [...Array(50)].map((_, index) => ({
11+
value: `Option ${index}`,
12+
label: `Option ${index}`,
13+
}));
14+
const options: SelectProps.Options = [
15+
{ value: 'first', label: 'Simple' },
16+
{ value: 'second', label: 'With small icon', iconName: 'folder' },
17+
{
18+
value: 'third',
19+
label: 'With big icon icon',
20+
description: 'Very big option',
21+
iconName: 'heart',
22+
disabled: false,
23+
disabledReason: 'disabled reason',
24+
tags: ['Cool', 'Intelligent', 'Cat'],
25+
},
26+
{
27+
label: 'Option group',
28+
options: [{ value: 'forth', label: 'Nested option' }],
29+
disabledReason: 'disabled reason',
30+
},
31+
...lotsOfOptions,
32+
{ label: 'Last option', disabled: false, disabledReason: 'disabled reason' },
33+
];
34+
35+
export default function SelectPage() {
36+
const [selectedOptions, setSelectedOptions] = React.useState<MultiselectProps.Options>([]);
37+
const renderOptionItem: MultiselectProps.MultiselectOptionItemRenderer = ({ item }) => {
38+
if (item.type === 'select-all') {
39+
return <div>Select all? {item.selected ? 'Yes' : 'No'}</div>;
40+
} else if (item.type === 'group') {
41+
return <div>Group: {item.option.label}</div>;
42+
} else if (item.type === 'item') {
43+
return <div>Item: {item.option.label}</div>;
44+
}
45+
46+
return null;
47+
};
48+
49+
return (
50+
<SimplePage title="Multiselect with custom item renderer" screenshotArea={{}}>
51+
<div style={{ inlineSize: '400px' }}>
52+
<Multiselect
53+
enableSelectAll={true}
54+
i18nStrings={{ ...i18nStrings, selectAllText: 'Select all' }}
55+
filteringType={'auto'}
56+
renderOption={renderOptionItem}
57+
placeholder="Choose option"
58+
selectedOptions={selectedOptions}
59+
onChange={event => {
60+
setSelectedOptions(event.detail.selectedOptions);
61+
}}
62+
options={options}
63+
/>
64+
</div>
65+
</SimplePage>
66+
);
67+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import * as React from 'react';
4+
import { useState } from 'react';
5+
6+
import Select, { SelectProps } from '~components/select';
7+
8+
import { SimplePage } from '../app/templates';
9+
10+
const lotsOfOptions = [...Array(50).keys()].map(n => {
11+
const numberToDisplay = (n + 5).toString();
12+
return {
13+
value: numberToDisplay,
14+
label: `Option ${n + 5}`,
15+
};
16+
});
17+
18+
const options: SelectProps.Options = [
19+
{ value: 'first', label: 'Simple' },
20+
{ value: 'second', label: 'With small icon', iconName: 'folder' },
21+
{
22+
value: 'third',
23+
label: 'With big icon icon',
24+
description: 'Very big option',
25+
iconName: 'heart',
26+
disabled: true,
27+
disabledReason: 'disabled reason',
28+
tags: ['Cool', 'Intelligent', 'Cat'],
29+
},
30+
{
31+
label: 'Option group',
32+
options: [{ value: 'forth', label: 'Nested option' }],
33+
disabledReason: 'disabled reason',
34+
},
35+
...lotsOfOptions,
36+
{ label: 'Last option', disabled: true, disabledReason: 'disabled reason' },
37+
];
38+
39+
export default function SelectPage() {
40+
const [selectedOption, setSelectedOption] = useState<SelectProps.Option | null>(null);
41+
const renderOption: SelectProps.SelectOptionItemRenderer = ({ item }) => {
42+
if (item.type === 'trigger') {
43+
return <div>Trigger: {item.option.label}</div>;
44+
} else if (item.type === 'group') {
45+
return <div>Group: {item.option.label}</div>;
46+
} else if (item.type === 'item') {
47+
return <div>Item: {item.option.label}</div>;
48+
}
49+
return null;
50+
};
51+
52+
return (
53+
<SimplePage title="Select with custom item renderer" screenshotArea={{}}>
54+
<div style={{ inlineSize: '400px' }}>
55+
<Select
56+
filteringType="auto"
57+
renderOption={renderOption}
58+
placeholder="Choose option"
59+
selectedOption={selectedOption}
60+
onChange={({ detail }) => setSelectedOption(detail.selectedOption)}
61+
options={options}
62+
triggerVariant="option"
63+
/>
64+
</div>
65+
</SimplePage>
66+
);
67+
}

pages/select/item.permutations.page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ const options: Record<string, DropdownOption> = {
8989
},
9090
};
9191

92-
const permutations = createPermutations<ItemProps>([
92+
const permutations = createPermutations<Omit<ItemProps, 'index'>>([
9393
{
9494
option: [options.simpleOption],
9595
highlighted: [false, true],
@@ -150,7 +150,10 @@ export default function InputPermutations() {
150150
<h1>Select item permutations</h1>
151151
<ScreenshotArea>
152152
<ul role="listbox" aria-label="list">
153-
<PermutationsView permutations={permutations} render={permutation => <Item {...permutation} />} />
153+
<PermutationsView
154+
permutations={permutations}
155+
render={(permutation, index) => <Item index={index ?? -1} {...permutation} />}
156+
/>
154157
</ul>
155158
</ScreenshotArea>
156159
</>

src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Jest Snapshot v1, https://goo.gl/fbAQLP
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

33
exports[`Components definition for alert matches the snapshot: alert 1`] = `
44
{
@@ -17806,6 +17806,26 @@ For more information, see the
1780617806
"optional": true,
1780717807
"type": "SelectProps.ContainingOptionAndGroupString",
1780817808
},
17809+
{
17810+
"description": "Specifies a render function to render custom options in the dropdown menu.",
17811+
"inlineType": {
17812+
"name": "MultiselectProps.MultiselectOptionItemRenderer",
17813+
"parameters": [
17814+
{
17815+
"name": "props",
17816+
"type": "{ item: MultiselectProps.MultiselectItem; filterText?: string | undefined; }",
17817+
},
17818+
],
17819+
"returnType": "React.ReactNode",
17820+
"type": "function",
17821+
},
17822+
"name": "renderOption",
17823+
"optional": true,
17824+
"systemTags": [
17825+
"core",
17826+
],
17827+
"type": "MultiselectProps.MultiselectOptionItemRenderer",
17828+
},
1780917829
{
1781017830
"description": "Specifies the localized string that describes an option as being selected.
1781117831
This is required to provide a good screen reader experience. For more information, see the
@@ -23351,6 +23371,26 @@ For more information, see the
2335123371
"optional": true,
2335223372
"type": "SelectProps.ContainingOptionAndGroupString",
2335323373
},
23374+
{
23375+
"description": "Specifies a render function to render custom options in the dropdown menu or trigger.",
23376+
"inlineType": {
23377+
"name": "SelectProps.SelectOptionItemRenderer",
23378+
"parameters": [
23379+
{
23380+
"name": "props",
23381+
"type": "{ item: SelectProps.SelectItem; filterText?: string | undefined; }",
23382+
},
23383+
],
23384+
"returnType": "React.ReactNode",
23385+
"type": "function",
23386+
},
23387+
"name": "renderOption",
23388+
"optional": true,
23389+
"systemTags": [
23390+
"core",
23391+
],
23392+
"type": "SelectProps.SelectOptionItemRenderer",
23393+
},
2335423394
{
2335523395
"description": "Specifies the localized string that describes an option as being selected.
2335623396
This is required to provide a good screen reader experience. For more information, see the
@@ -32404,6 +32444,7 @@ Options get highlighted when they match the value of the input field.",
3240432444
},
3240532445
},
3240632446
{
32447+
"description": "Finds the label wrapper of this option.",
3240732448
"name": "findLabel",
3240832449
"parameters": [],
3240932450
"returnType": {
@@ -43424,6 +43465,7 @@ Options get highlighted when they match the value of the input field.",
4342443465
},
4342543466
},
4342643467
{
43468+
"description": "Finds the label wrapper of this option.",
4342743469
"name": "findLabel",
4342843470
"parameters": [],
4342943471
"returnType": {

src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ exports[`test-utils selectors 1`] = `
366366
"awsui_chart-filter_1px7g",
367367
"awsui_content_x6dl3",
368368
"awsui_control_1wepg",
369+
"awsui_custom-content_1p2cx",
369370
"awsui_description_1p2cx",
370371
"awsui_description_1wepg",
371372
"awsui_direction-button-block-end_8k1rt",

src/internal/components/button-trigger/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface ButtonTriggerProps extends BaseComponentProps {
3737
onClick?: CancelableEventHandler;
3838
onFocus?: CancelableEventHandler;
3939
onBlur?: CancelableEventHandler<{ relatedTarget: Node | null }>;
40+
hasCustomContent?: boolean;
4041
autoFocus?: boolean;
4142
}
4243

@@ -62,6 +63,7 @@ const ButtonTrigger = (
6263
onClick,
6364
onFocus,
6465
onBlur,
66+
hasCustomContent = false,
6567
autoFocus,
6668
...restProps
6769
}: ButtonTriggerProps,
@@ -83,7 +85,8 @@ const ButtonTrigger = (
8385
readOnly && styles.readonly,
8486
inFilteringToken && styles['in-filtering-token'],
8587
inFilteringToken && styles[`in-filtering-token-${inFilteringToken}`],
86-
inlineTokens && styles['inline-tokens']
88+
inlineTokens && styles['inline-tokens'],
89+
!!hasCustomContent && styles['custom-option']
8790
),
8891
disabled: disabled,
8992
'aria-expanded': pressed,

src/internal/components/button-trigger/styles.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ $padding-block-inner-filtering-token: 0px;
133133
}
134134
}
135135

136+
&.custom-option {
137+
// Remove default paddings for custom options
138+
padding-block: 0;
139+
padding-inline-start: 0;
140+
overflow: clip;
141+
}
142+
136143
&.inline-tokens {
137144
// Remove default paddings and just rely on center alignment of the content
138145
padding-block: 0;

src/internal/components/option/__tests__/option.test.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ describe('Option component', () => {
4343
value: 'optionABC',
4444
label: 'ABC',
4545
};
46+
test('displays custom content', () => {
47+
const optionWrapper = renderOption({
48+
customContent: <div>My Custom Content</div>,
49+
option: {
50+
...baseOption,
51+
},
52+
});
53+
expect(optionWrapper.findLabel()!.getElement()).toHaveTextContent('My Custom Content');
54+
expect(optionWrapper!.getElement()).toHaveTextContent('My Custom Content');
55+
});
56+
4657
test('displays label', () => {
4758
const optionWrapper = renderOption({
4859
option: baseOption,

src/internal/components/option/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,20 @@ const Option = ({
3535
labelContainerRef,
3636
labelRef,
3737
labelId,
38+
customContent,
3839
...restProps
3940
}: OptionProps) => {
4041
if (!option) {
4142
return null;
4243
}
44+
if (customContent) {
45+
return (
46+
<div data-value={option.value} className={clsx(styles.option)}>
47+
<div className={clsx(styles['custom-content'])}>{customContent}</div>
48+
</div>
49+
);
50+
}
51+
4352
const { disabled } = option;
4453
const baseProps = getBaseProps(restProps);
4554
const SpanOrDivTag = option.labelContent ? 'div' : 'span';
@@ -55,7 +64,6 @@ const Option = ({
5564
validateStringValue(tag, `filteringTags[${index}]`);
5665
});
5766
}
58-
5967
const className = clsx(
6068
styles.option,
6169
disabled && styles.disabled,

0 commit comments

Comments
 (0)