Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nice-tires-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@easypost/easy-ui": minor
---

add support for secondary text on select items
8 changes: 7 additions & 1 deletion easy-ui-react/src/Select/Select.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { Canvas, Meta, ArgTypes } from "@storybook/addon-docs/blocks";
import { Canvas, Meta, ArgTypes, Controls } from "@storybook/addon-docs/blocks";
import { Select } from "./Select";
import * as SelectStories from "./Select.stories";

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

<Canvas of={SelectStories.DisabledSelect} />

## With Secondary Text

Each option can render a secondary description. The selected value in the closed field also shows the same secondary text.

<Canvas of={SelectStories.WithSecondaryText} />

## Properties

### Select
Expand Down
73 changes: 57 additions & 16 deletions easy-ui-react/src/Select/Select.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,65 @@
position: fixed;
inset: 0;
}

.listboxRoot {
@include Menu.root;
}

.listbox {
@include Menu.menu;
outline: none;
}

.listboxList,
.sectionList {
@include Menu.menuList;
}

.optionContent,
.separator {
@include Menu.itemContent;
.listboxList {
padding-top: 0;
padding-bottom: 0;
}

.option {
text-decoration: none;
}
outline: none;

.optionContent {
@include Menu.itemContentColor;
}
&:not([data-is-disabled="true"]) {
cursor: pointer;
}

.listbox,
.option {
outline: none;
&[data-is-focused="true"] .optionContent {
@include Menu.itemDataFocused;
}

&[data-is-disabled="true"] .optionContent {
@include Menu.itemDataDisabled;
}
}

.option:not([data-is-disabled="true"]) {
cursor: pointer;
.optionContent,
.separator {
@include Menu.itemContent;
}

.option[data-is-focused="true"] .optionContent {
@include Menu.itemDataFocused;
.optionContent {
@include Menu.itemContentColor;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
min-width: 0;
width: 100%;
height: auto;
padding-top: design-token("space.1");
padding-bottom: design-token("space.1");
}

.option[data-is-disabled="true"] .optionContent {
@include Menu.itemDataDisabled;
.optionDescription {
width: 100%;
line-height: 1.2;
white-space: nowrap;
}

.separator::after {
Expand All @@ -60,6 +78,8 @@
.selectField {
display: flex;
align-items: center;
flex: 1 1 auto;
min-width: 0;
@include Input.input;

&:hover {
Expand Down Expand Up @@ -93,8 +113,29 @@

.selectFieldText {
overflow: hidden;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
min-width: 0;
width: 100%;
text-align: left;
gap: 0;
}

.selectedDescription {
width: 100%;
line-height: 1.2;
white-space: nowrap;
}

.listboxOpen {
border-color: component-token("inputfield", "color.border.engaged");
}

// Override the absolute-positioned end icon to vertically center
// when the trigger grows taller (e.g. descriptive two-line mode)
.selectFieldIconContainer > :last-child {
top: 50%;
transform: translateY(-50%);
}
38 changes: 38 additions & 0 deletions easy-ui-react/src/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,41 @@ export const DisabledSelect: Story = {
isDisabled: true,
},
};

export const WithSecondaryText: Story = {
render: () => {
const [selectedKey, setSelectedKey] = React.useState<Key>("standard");

return (
<Select
label="Rate adjustment"
selectedKey={selectedKey}
onSelectionChange={(selected) => setSelectedKey(selected)}
>
<Select.Option
key="standard"
textValue="Standard Adjustment"
description="One Rate Adjustment criteria is followed and added to all qualifying shipments"
>
Standard Adjustment
</Select.Option>

<Select.Option
key="cumulative"
textValue="Cumulative Adjustment"
description="Multiple Rate Adjustments are added together to create one total rate markup amount"
>
Cumulative Adjustment
</Select.Option>

<Select.Option
key="hierarchical"
textValue="Hierarchical Adjustment"
description="One Rate Adjustment will be added based on a priority order of possible Rate Adjustments"
>
Hierarchical Adjustment
</Select.Option>
</Select>
);
},
};
38 changes: 38 additions & 0 deletions easy-ui-react/src/Select/Select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,44 @@ describe("<Select />", () => {
await clickElement(user, screen.queryAllByRole("option")[0]);
expect(handleSelectionChange).toHaveBeenCalledTimes(1);
});
it("should render option descriptions in the dropdown", async () => {
const { user } = render(
getSelect({
options: [
<Select.Option key="1" description="First description">
Option 1
</Select.Option>,
<Select.Option key="2" description="Second description">
Option 2
</Select.Option>,
],
}),
);

await clickElement(user, screen.getByRole("button"));

expect(screen.getByText("First description")).toBeInTheDocument();
expect(screen.getByText("Second description")).toBeInTheDocument();
});

it("should render selected option description in the closed field", () => {
render(
getSelect({
selectProps: { defaultSelectedKey: "1" },
options: [
<Select.Option key="1" description="First description">
Option 1
</Select.Option>,
<Select.Option key="2" description="Second description">
Option 2
</Select.Option>,
],
}),
);

expect(screen.getAllByText("Option 1")[0]).toBeInTheDocument();
expect(screen.getByText("First description")).toBeInTheDocument();
});
});

function getSelect({
Expand Down
101 changes: 51 additions & 50 deletions easy-ui-react/src/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,56 +43,56 @@ export type SelectProps<T, K extends Key> = Omit<
*
* @example
* _Simple controlled selection:_
*```tsx
* import { Select } from "@easypost/easy-ui/Select";
*
* export function Component() {
* const [selectedOption, setSelectedOption] = React.useState("Option 1");
*
* return (
* <Select
* label="Label"
* selectedKey={selectedOption}
* onSelectionChange={(selected) => setSelectedOption(selected)}
* helperText="Helper text"
* >
* <Select.Option key="Option 1">Option 1</Select.Option>
* <Select.Option key="Option 2">Option 2</Select.Option>
* <Select.Option key="Option 3">Option 3</Select.Option>
* </Select>
* );
* }
```
*
* @example
* _Simple controlled selection with separator:_
*```tsx
* import { Select } from "@easypost/easy-ui/Select";
*
* export function Component() {
* const [selectedOption, setSelectedOption] = React.useState("Option 1");
*
* return (
* <Select
* label="Label"
* selectedKey={selectedOption}
* onSelectionChange={(selected) => setSelectedOption(selected)}
* helperText="Helper text"
* >
* <Select.Section aria-label="Primary options">
* <Select.Option key="Option 1">Option 1</Select.Option>
* <Select.Option key="Option 2">Option 2</Select.Option>
* <Select.Option key="Option 3">Option 3</Select.Option>
* </Select.Section>
* <Select.Section aria-label="Secondary options">
* <Select.Option key="Option 4">Option 4</Select.Option>
* <Select.Option key="Option 5">Option 5</Select.Option>
* <Select.Option key="Option 6">Option 6</Select.Option>
* </Select.Section>
* </Select>
* );
* }
```
*```tsx
* import { Select } from "@easypost/easy-ui/Select";
*
* export function Component() {
* const [selectedOption, setSelectedOption] = React.useState("Option 1");
*
* return (
* <Select
* label="Label"
* selectedKey={selectedOption}
* onSelectionChange={(selected) => setSelectedOption(selected)}
* helperText="Helper text"
* >
* <Select.Option key="Option 1">Option 1</Select.Option>
* <Select.Option key="Option 2">Option 2</Select.Option>
* <Select.Option key="Option 3">Option 3</Select.Option>
* </Select>
* );
* }
* ```
*
* @example
* _Simple controlled selection with separator:_
*```tsx
* import { Select } from "@easypost/easy-ui/Select";
*
* export function Component() {
* const [selectedOption, setSelectedOption] = React.useState("Option 1");
*
* return (
* <Select
* label="Label"
* selectedKey={selectedOption}
* onSelectionChange={(selected) => setSelectedOption(selected)}
* helperText="Helper text"
* >
* <Select.Section aria-label="Primary options">
* <Select.Option key="Option 1">Option 1</Select.Option>
* <Select.Option key="Option 2">Option 2</Select.Option>
* <Select.Option key="Option 3">Option 3</Select.Option>
* </Select.Section>
* <Select.Section aria-label="Secondary options">
* <Select.Option key="Option 4">Option 4</Select.Option>
* <Select.Option key="Option 5">Option 5</Select.Option>
* <Select.Option key="Option 6">Option 6</Select.Option>
* </Select.Section>
* </Select>
* );
* }
* ```
*/
export function Select<T extends object, K extends Key>(
props: SelectProps<T, K>,
Expand Down Expand Up @@ -165,6 +165,7 @@ export function Select<T extends object, K extends Key>(
</InternalSelectContext.Provider>
);
}

/**
* Represents a section in a `<Select />`.
*
Expand Down
16 changes: 15 additions & 1 deletion easy-ui-react/src/Select/SelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ export function SelectField(props: SelectFieldProps) {
const showHelperText = !showErrorText && helperText;
const captionProps = showHelperText ? helperTextProps : errorTextProps;
const captionText = showHelperText ? helperText : errorText;
const selectedDescription = (
selectState.selectedItem?.props as { description?: ReactNode } | undefined
)?.description;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is it possible to render the descriptive secondary text without passing isDescriptive to Select?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching that it ended up being dead code after my final changes


return (
<div className={classNames(styles.fieldRoot)}>
Expand Down Expand Up @@ -119,7 +122,18 @@ export function SelectField(props: SelectFieldProps) {
size={size}
>
{selectState.selectedItem ? (
selectState.selectedItem.rendered
<>
<Text variant="body2" truncate>
{selectState.selectedItem.rendered}
</Text>
{selectedDescription && (
<span className={styles.selectedDescription}>
<Text variant="caption" color="neutral.600" truncate>
{selectedDescription}
</Text>
</span>
)}
</>
) : (
<Text color="neutral.600">{placeholder}</Text>
)}
Expand Down
Loading
Loading