Skip to content

Commit 6ca337d

Browse files
authored
add support for secondary text on select items (#1883)
## 📝 Changes Please provide a brief summary of the changes made and why they were made. Include any notes, screenshots, or videos that may be helpful for developers reviewing this pull request. ## ✅ Checklist Easy UI has certain UX standards that must be met. In general, non-trivial changes should meet the following criteria: - [ ] Visuals match Design Specs in Figma - [ ] Stories accompany any component changes - [ ] Code is in accordance with our style guide - [ ] Design tokens are utilized - [ ] Unit tests accompany any component changes - [ ] TSDoc is written for any API surface area - [ ] Specs are up-to-date - [ ] Console is free from warnings - [ ] No accessibility violations are reported - [ ] Cross-browser check is performed (Chrome, Safari, Firefox) - [ ] Changeset is added ~Strikethrough~ any items that are not applicable to this pull request.
1 parent 65121f3 commit 6ca337d

9 files changed

Lines changed: 229 additions & 73 deletions

File tree

.changeset/nice-tires-trade.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@easypost/easy-ui": minor
3+
---
4+
5+
add support for secondary text on select items

easy-ui-react/src/Select/Select.mdx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react";
2-
import { Canvas, Meta, ArgTypes } from "@storybook/addon-docs/blocks";
2+
import { Canvas, Meta, ArgTypes, Controls } from "@storybook/addon-docs/blocks";
33
import { Select } from "./Select";
44
import * as SelectStories from "./Select.stories";
55

@@ -76,6 +76,12 @@ Use `isDisabled` to disabled the field entirely.
7676

7777
<Canvas of={SelectStories.DisabledSelect} />
7878

79+
## With Secondary Text
80+
81+
Each option can render a secondary description. The selected value in the closed field also shows the same secondary text.
82+
83+
<Canvas of={SelectStories.WithSecondaryText} />
84+
7985
## Properties
8086

8187
### Select

easy-ui-react/src/Select/Select.module.scss

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,65 @@
66
position: fixed;
77
inset: 0;
88
}
9+
910
.listboxRoot {
1011
@include Menu.root;
1112
}
1213

1314
.listbox {
1415
@include Menu.menu;
16+
outline: none;
1517
}
1618

1719
.listboxList,
1820
.sectionList {
1921
@include Menu.menuList;
2022
}
2123

22-
.optionContent,
23-
.separator {
24-
@include Menu.itemContent;
24+
.listboxList {
25+
padding-top: 0;
26+
padding-bottom: 0;
2527
}
2628

2729
.option {
2830
text-decoration: none;
29-
}
31+
outline: none;
3032

31-
.optionContent {
32-
@include Menu.itemContentColor;
33-
}
33+
&:not([data-is-disabled="true"]) {
34+
cursor: pointer;
35+
}
3436

35-
.listbox,
36-
.option {
37-
outline: none;
37+
&[data-is-focused="true"] .optionContent {
38+
@include Menu.itemDataFocused;
39+
}
40+
41+
&[data-is-disabled="true"] .optionContent {
42+
@include Menu.itemDataDisabled;
43+
}
3844
}
3945

40-
.option:not([data-is-disabled="true"]) {
41-
cursor: pointer;
46+
.optionContent,
47+
.separator {
48+
@include Menu.itemContent;
4249
}
4350

44-
.option[data-is-focused="true"] .optionContent {
45-
@include Menu.itemDataFocused;
51+
.optionContent {
52+
@include Menu.itemContentColor;
53+
display: flex;
54+
flex-direction: column;
55+
align-items: flex-start;
56+
gap: 2px;
57+
min-width: 0;
58+
width: 100%;
59+
height: auto;
60+
padding-top: design-token("space.1");
61+
padding-bottom: design-token("space.1");
4662
}
4763

48-
.option[data-is-disabled="true"] .optionContent {
49-
@include Menu.itemDataDisabled;
64+
.optionDescription {
65+
width: 100%;
66+
line-height: 1.2;
67+
white-space: nowrap;
5068
}
5169

5270
.separator::after {
@@ -60,6 +78,8 @@
6078
.selectField {
6179
display: flex;
6280
align-items: center;
81+
flex: 1 1 auto;
82+
min-width: 0;
6383
@include Input.input;
6484

6585
&:hover {
@@ -93,8 +113,29 @@
93113

94114
.selectFieldText {
95115
overflow: hidden;
116+
display: flex;
117+
flex-direction: column;
118+
align-items: flex-start;
119+
justify-content: center;
120+
min-width: 0;
121+
width: 100%;
122+
text-align: left;
123+
gap: 0;
124+
}
125+
126+
.selectedDescription {
127+
width: 100%;
128+
line-height: 1.2;
129+
white-space: nowrap;
96130
}
97131

98132
.listboxOpen {
99133
border-color: component-token("inputfield", "color.border.engaged");
100134
}
135+
136+
// Override the absolute-positioned end icon to vertically center
137+
// when the trigger grows taller (e.g. descriptive two-line mode)
138+
.selectFieldIconContainer > :last-child {
139+
top: 50%;
140+
transform: translateY(-50%);
141+
}

easy-ui-react/src/Select/Select.stories.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,41 @@ export const DisabledSelect: Story = {
187187
isDisabled: true,
188188
},
189189
};
190+
191+
export const WithSecondaryText: Story = {
192+
render: () => {
193+
const [selectedKey, setSelectedKey] = React.useState<Key>("standard");
194+
195+
return (
196+
<Select
197+
label="Rate adjustment"
198+
selectedKey={selectedKey}
199+
onSelectionChange={(selected) => setSelectedKey(selected)}
200+
>
201+
<Select.Option
202+
key="standard"
203+
textValue="Standard Adjustment"
204+
description="One Rate Adjustment criteria is followed and added to all qualifying shipments"
205+
>
206+
Standard Adjustment
207+
</Select.Option>
208+
209+
<Select.Option
210+
key="cumulative"
211+
textValue="Cumulative Adjustment"
212+
description="Multiple Rate Adjustments are added together to create one total rate markup amount"
213+
>
214+
Cumulative Adjustment
215+
</Select.Option>
216+
217+
<Select.Option
218+
key="hierarchical"
219+
textValue="Hierarchical Adjustment"
220+
description="One Rate Adjustment will be added based on a priority order of possible Rate Adjustments"
221+
>
222+
Hierarchical Adjustment
223+
</Select.Option>
224+
</Select>
225+
);
226+
},
227+
};

easy-ui-react/src/Select/Select.test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,44 @@ describe("<Select />", () => {
102102
await clickElement(user, screen.queryAllByRole("option")[0]);
103103
expect(handleSelectionChange).toHaveBeenCalledTimes(1);
104104
});
105+
it("should render option descriptions in the dropdown", async () => {
106+
const { user } = render(
107+
getSelect({
108+
options: [
109+
<Select.Option key="1" description="First description">
110+
Option 1
111+
</Select.Option>,
112+
<Select.Option key="2" description="Second description">
113+
Option 2
114+
</Select.Option>,
115+
],
116+
}),
117+
);
118+
119+
await clickElement(user, screen.getByRole("button"));
120+
121+
expect(screen.getByText("First description")).toBeInTheDocument();
122+
expect(screen.getByText("Second description")).toBeInTheDocument();
123+
});
124+
125+
it("should render selected option description in the closed field", () => {
126+
render(
127+
getSelect({
128+
selectProps: { defaultSelectedKey: "1" },
129+
options: [
130+
<Select.Option key="1" description="First description">
131+
Option 1
132+
</Select.Option>,
133+
<Select.Option key="2" description="Second description">
134+
Option 2
135+
</Select.Option>,
136+
],
137+
}),
138+
);
139+
140+
expect(screen.getAllByText("Option 1")[0]).toBeInTheDocument();
141+
expect(screen.getByText("First description")).toBeInTheDocument();
142+
});
105143
});
106144

107145
function getSelect({

easy-ui-react/src/Select/Select.tsx

Lines changed: 51 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -43,56 +43,56 @@ export type SelectProps<T, K extends Key> = Omit<
4343
*
4444
* @example
4545
* _Simple controlled selection:_
46-
*```tsx
47-
* import { Select } from "@easypost/easy-ui/Select";
48-
*
49-
* export function Component() {
50-
* const [selectedOption, setSelectedOption] = React.useState("Option 1");
51-
*
52-
* return (
53-
* <Select
54-
* label="Label"
55-
* selectedKey={selectedOption}
56-
* onSelectionChange={(selected) => setSelectedOption(selected)}
57-
* helperText="Helper text"
58-
* >
59-
* <Select.Option key="Option 1">Option 1</Select.Option>
60-
* <Select.Option key="Option 2">Option 2</Select.Option>
61-
* <Select.Option key="Option 3">Option 3</Select.Option>
62-
* </Select>
63-
* );
64-
* }
65-
```
66-
*
67-
* @example
68-
* _Simple controlled selection with separator:_
69-
*```tsx
70-
* import { Select } from "@easypost/easy-ui/Select";
71-
*
72-
* export function Component() {
73-
* const [selectedOption, setSelectedOption] = React.useState("Option 1");
74-
*
75-
* return (
76-
* <Select
77-
* label="Label"
78-
* selectedKey={selectedOption}
79-
* onSelectionChange={(selected) => setSelectedOption(selected)}
80-
* helperText="Helper text"
81-
* >
82-
* <Select.Section aria-label="Primary options">
83-
* <Select.Option key="Option 1">Option 1</Select.Option>
84-
* <Select.Option key="Option 2">Option 2</Select.Option>
85-
* <Select.Option key="Option 3">Option 3</Select.Option>
86-
* </Select.Section>
87-
* <Select.Section aria-label="Secondary options">
88-
* <Select.Option key="Option 4">Option 4</Select.Option>
89-
* <Select.Option key="Option 5">Option 5</Select.Option>
90-
* <Select.Option key="Option 6">Option 6</Select.Option>
91-
* </Select.Section>
92-
* </Select>
93-
* );
94-
* }
95-
```
46+
*```tsx
47+
* import { Select } from "@easypost/easy-ui/Select";
48+
*
49+
* export function Component() {
50+
* const [selectedOption, setSelectedOption] = React.useState("Option 1");
51+
*
52+
* return (
53+
* <Select
54+
* label="Label"
55+
* selectedKey={selectedOption}
56+
* onSelectionChange={(selected) => setSelectedOption(selected)}
57+
* helperText="Helper text"
58+
* >
59+
* <Select.Option key="Option 1">Option 1</Select.Option>
60+
* <Select.Option key="Option 2">Option 2</Select.Option>
61+
* <Select.Option key="Option 3">Option 3</Select.Option>
62+
* </Select>
63+
* );
64+
* }
65+
* ```
66+
*
67+
* @example
68+
* _Simple controlled selection with separator:_
69+
*```tsx
70+
* import { Select } from "@easypost/easy-ui/Select";
71+
*
72+
* export function Component() {
73+
* const [selectedOption, setSelectedOption] = React.useState("Option 1");
74+
*
75+
* return (
76+
* <Select
77+
* label="Label"
78+
* selectedKey={selectedOption}
79+
* onSelectionChange={(selected) => setSelectedOption(selected)}
80+
* helperText="Helper text"
81+
* >
82+
* <Select.Section aria-label="Primary options">
83+
* <Select.Option key="Option 1">Option 1</Select.Option>
84+
* <Select.Option key="Option 2">Option 2</Select.Option>
85+
* <Select.Option key="Option 3">Option 3</Select.Option>
86+
* </Select.Section>
87+
* <Select.Section aria-label="Secondary options">
88+
* <Select.Option key="Option 4">Option 4</Select.Option>
89+
* <Select.Option key="Option 5">Option 5</Select.Option>
90+
* <Select.Option key="Option 6">Option 6</Select.Option>
91+
* </Select.Section>
92+
* </Select>
93+
* );
94+
* }
95+
* ```
9696
*/
9797
export function Select<T extends object, K extends Key>(
9898
props: SelectProps<T, K>,
@@ -165,6 +165,7 @@ export function Select<T extends object, K extends Key>(
165165
</InternalSelectContext.Provider>
166166
);
167167
}
168+
168169
/**
169170
* Represents a section in a `<Select />`.
170171
*

easy-ui-react/src/Select/SelectField.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ export function SelectField(props: SelectFieldProps) {
9292
const showHelperText = !showErrorText && helperText;
9393
const captionProps = showHelperText ? helperTextProps : errorTextProps;
9494
const captionText = showHelperText ? helperText : errorText;
95+
const selectedDescription = (
96+
selectState.selectedItem?.props as { description?: ReactNode } | undefined
97+
)?.description;
9598

9699
return (
97100
<div className={classNames(styles.fieldRoot)}>
@@ -119,7 +122,18 @@ export function SelectField(props: SelectFieldProps) {
119122
size={size}
120123
>
121124
{selectState.selectedItem ? (
122-
selectState.selectedItem.rendered
125+
<>
126+
<Text variant="body2" truncate>
127+
{selectState.selectedItem.rendered}
128+
</Text>
129+
{selectedDescription && (
130+
<span className={styles.selectedDescription}>
131+
<Text variant="caption" color="neutral.600" truncate>
132+
{selectedDescription}
133+
</Text>
134+
</span>
135+
)}
136+
</>
123137
) : (
124138
<Text color="neutral.600">{placeholder}</Text>
125139
)}

0 commit comments

Comments
 (0)