From e250c52f26ce8c5a54cb9d57d59defbee8ac692b Mon Sep 17 00:00:00 2001 From: jallen Date: Wed, 8 Apr 2026 17:12:55 -0400 Subject: [PATCH 1/4] add support for secondary text on select items --- .changeset/nice-tires-trade.md | 5 + easy-ui-react/src/Select/Select.mdx | 8 +- easy-ui-react/src/Select/Select.module.scss | 77 ++++++++++--- easy-ui-react/src/Select/Select.stories.tsx | 39 +++++++ easy-ui-react/src/Select/Select.test.tsx | 38 +++++++ easy-ui-react/src/Select/Select.tsx | 114 +++++++++++--------- easy-ui-react/src/Select/SelectContext.tsx | 1 + easy-ui-react/src/Select/SelectField.tsx | 21 +++- easy-ui-react/src/Select/SelectOption.tsx | 17 ++- easy-ui-react/src/Select/SelectTrigger.tsx | 6 +- 10 files changed, 251 insertions(+), 75 deletions(-) create mode 100644 .changeset/nice-tires-trade.md diff --git a/.changeset/nice-tires-trade.md b/.changeset/nice-tires-trade.md new file mode 100644 index 000000000..2195bab8d --- /dev/null +++ b/.changeset/nice-tires-trade.md @@ -0,0 +1,5 @@ +--- +"@easypost/easy-ui": minor +--- + +add support for secondary text on select items diff --git a/easy-ui-react/src/Select/Select.mdx b/easy-ui-react/src/Select/Select.mdx index 63cc79c97..44c13c477 100644 --- a/easy-ui-react/src/Select/Select.mdx +++ b/easy-ui-react/src/Select/Select.mdx @@ -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"; @@ -76,6 +76,12 @@ Use `isDisabled` to disabled the field entirely. +## With Secondary Text + +Each option can render a secondary description. The selected value in the closed field also shows the same secondary text. + + + ## Properties ### Select diff --git a/easy-ui-react/src/Select/Select.module.scss b/easy-ui-react/src/Select/Select.module.scss index a6a7200ae..b4b2482b0 100644 --- a/easy-ui-react/src/Select/Select.module.scss +++ b/easy-ui-react/src/Select/Select.module.scss @@ -6,6 +6,7 @@ position: fixed; inset: 0; } + .listboxRoot { @include Menu.root; } @@ -19,40 +20,61 @@ @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 { @include Menu.separator; } +.listbox, +.option { + outline: none; +} + .fieldRoot { @include Input.root; } @@ -60,6 +82,8 @@ .selectField { display: flex; align-items: center; + flex: 1 1 auto; + min-width: 0; @include Input.input; &:hover { @@ -93,8 +117,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%); +} diff --git a/easy-ui-react/src/Select/Select.stories.tsx b/easy-ui-react/src/Select/Select.stories.tsx index 12955eeec..6ad4c800d 100644 --- a/easy-ui-react/src/Select/Select.stories.tsx +++ b/easy-ui-react/src/Select/Select.stories.tsx @@ -187,3 +187,42 @@ export const DisabledSelect: Story = { isDisabled: true, }, }; + +export const WithSecondaryText: Story = { + render: () => { + const [selectedKey, setSelectedKey] = React.useState("standard"); + + return ( + + ); + }, +}; \ No newline at end of file diff --git a/easy-ui-react/src/Select/Select.test.tsx b/easy-ui-react/src/Select/Select.test.tsx index d13c80fc0..336ad0ee3 100644 --- a/easy-ui-react/src/Select/Select.test.tsx +++ b/easy-ui-react/src/Select/Select.test.tsx @@ -102,6 +102,44 @@ describe(" setSelectedOption(selected)} -* helperText="Helper text" -* > -* Option 1 -* Option 2 -* Option 3 -* -* ); -* } -``` -* -* @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 ( -* -* ); -* } -``` + *```tsx + * import { Select } from "@easypost/easy-ui/Select"; + * + * export function Component() { + * const [selectedOption, setSelectedOption] = React.useState("Option 1"); + * + * return ( + * + * ); + * } + * ``` + * + * @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 ( + * + * ); + * } + * ``` */ export function Select( - props: SelectProps, + props: SelectProps ) { const { isDisabled, @@ -108,6 +108,7 @@ export function Select( helperText, placeholder, iconAtStart, + isDescriptive = false, } = props; const triggerRef = React.useRef(null); @@ -141,8 +142,15 @@ export function Select( triggerRef, selectState, triggerWidth, + isDescriptive, }; - }, [triggerProps, listBoxPropsFromSelect, selectState, triggerWidth]); + }, [ + triggerProps, + listBoxPropsFromSelect, + selectState, + triggerWidth, + isDescriptive, + ]); return ( @@ -156,6 +164,7 @@ export function Select( helperText={helperText} placeholder={placeholder} iconAtStart={iconAtStart} + isDescriptive={isDescriptive} labelProps={labelProps} valueProps={valueProps} helperTextProps={helperTextProps} @@ -165,6 +174,7 @@ export function Select( ); } + /** * Represents a section in a ` ); }, -}; \ No newline at end of file +}; diff --git a/easy-ui-react/src/Select/Select.test.tsx b/easy-ui-react/src/Select/Select.test.tsx index 336ad0ee3..f0c675037 100644 --- a/easy-ui-react/src/Select/Select.test.tsx +++ b/easy-ui-react/src/Select/Select.test.tsx @@ -115,13 +115,13 @@ describe("", () => { ], }), ); - + expect(screen.getAllByText("Option 1")[0]).toBeInTheDocument(); expect(screen.getByText("First description")).toBeInTheDocument(); }); diff --git a/easy-ui-react/src/Select/Select.tsx b/easy-ui-react/src/Select/Select.tsx index 837f5d347..bfc46eb72 100644 --- a/easy-ui-react/src/Select/Select.tsx +++ b/easy-ui-react/src/Select/Select.tsx @@ -95,7 +95,7 @@ export type SelectProps = Omit< * ``` */ export function Select( - props: SelectProps + props: SelectProps, ) { const { isDisabled, From 6502f0f3e6782f8c8243641c12b69a1524734ddd Mon Sep 17 00:00:00 2001 From: jallen Date: Thu, 9 Apr 2026 10:22:12 -0400 Subject: [PATCH 4/4] review comment --- easy-ui-react/src/Select/Select.stories.tsx | 1 - easy-ui-react/src/Select/Select.tsx | 11 +---------- easy-ui-react/src/Select/SelectContext.tsx | 1 - easy-ui-react/src/Select/SelectField.tsx | 5 ----- 4 files changed, 1 insertion(+), 17 deletions(-) diff --git a/easy-ui-react/src/Select/Select.stories.tsx b/easy-ui-react/src/Select/Select.stories.tsx index c8de0e1f0..f3e5bf829 100644 --- a/easy-ui-react/src/Select/Select.stories.tsx +++ b/easy-ui-react/src/Select/Select.stories.tsx @@ -197,7 +197,6 @@ export const WithSecondaryText: Story = { label="Rate adjustment" selectedKey={selectedKey} onSelectionChange={(selected) => setSelectedKey(selected)} - isDescriptive > ( helperText, placeholder, iconAtStart, - isDescriptive = false, } = props; const triggerRef = React.useRef(null); @@ -142,15 +141,8 @@ export function Select( triggerRef, selectState, triggerWidth, - isDescriptive, }; - }, [ - triggerProps, - listBoxPropsFromSelect, - selectState, - triggerWidth, - isDescriptive, - ]); + }, [triggerProps, listBoxPropsFromSelect, selectState, triggerWidth]); return ( @@ -164,7 +156,6 @@ export function Select( helperText={helperText} placeholder={placeholder} iconAtStart={iconAtStart} - isDescriptive={isDescriptive} labelProps={labelProps} valueProps={valueProps} helperTextProps={helperTextProps} diff --git a/easy-ui-react/src/Select/SelectContext.tsx b/easy-ui-react/src/Select/SelectContext.tsx index 3a42ebb02..12008082e 100644 --- a/easy-ui-react/src/Select/SelectContext.tsx +++ b/easy-ui-react/src/Select/SelectContext.tsx @@ -8,7 +8,6 @@ type InternalSelectContextType = { triggerRef: MutableRefObject; selectState: SelectState; triggerWidth: number | null; - isDescriptive?: boolean; }; export const InternalSelectContext = diff --git a/easy-ui-react/src/Select/SelectField.tsx b/easy-ui-react/src/Select/SelectField.tsx index 77bba84fb..e94d15c14 100644 --- a/easy-ui-react/src/Select/SelectField.tsx +++ b/easy-ui-react/src/Select/SelectField.tsx @@ -54,11 +54,6 @@ export type BaseSelectFieldProps = { placeholder?: string; /** Left aligned icon on the select field. */ iconAtStart?: IconSymbol; - /** - * Whether the select field options include descriptive secondary text. - * @default false - */ - isDescriptive?: boolean; }; type SelectFieldAttributeProps = {