Skip to content

Commit

Permalink
feat: MultiSelect component (#1572)
Browse files Browse the repository at this point in the history
## πŸ“ Changes

- Adds `<MultiSelect />` component

## βœ… Checklist

Easy UI has certain UX standards that must be met. In general,
non-trivial changes should meet the following criteria:

- [x] Visuals match Design Specs in Figma
- [x] Stories accompany any component changes
- [x] Code is in accordance with our style guide
- [x] Design tokens are utilized
- [x] Unit tests accompany any component changes
- [x] TSDoc is written for any API surface area
- [x] Specs are up-to-date
- [x] Console is free from warnings
- [x] No accessibility violations are reported
- [x] Cross-browser check is performed (Chrome, Safari, Firefox)
- [x] Changeset is added

~Strikethrough~ any items that are not applicable to this pull request.
  • Loading branch information
stephenjwatkins authored Jan 15, 2025
1 parent c6a42f1 commit 74272b9
Show file tree
Hide file tree
Showing 17 changed files with 1,128 additions and 48 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-doors-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@easypost/easy-ui": minor
---

feat: MultiSelect component
1 change: 0 additions & 1 deletion easy-ui-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
"@types/react-transition-group": "^4.4.12",
"@vitejs/plugin-react": "^4.3.4",
"glob": "^10.2.5",
"jsdom": "^26.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.83.4",
Expand Down
6 changes: 2 additions & 4 deletions easy-ui-react/src/Menu/MenuOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,11 @@ import {
DEFAULT_MAX_ITEMS_UNTIL_SCROLL,
DEFAULT_PLACEMENT,
DEFAULT_WIDTH,
ITEM_HEIGHT,
OVERLAY_OFFSET,
OVERLAY_PADDING_FROM_CONTAINER,
SELECT_ALL_KEY,
Y_PADDING_INSIDE_OVERLAY,
filterSelectedKeys,
getMenuPopoverMaxHeight,
getUnmergedPopoverStyles,
isSelectAllSelected,
useSelectionCapture,
Expand Down Expand Up @@ -125,8 +124,7 @@ function MenuOverlayContent<T extends object>(props: MenuOverlayProps<T>) {
const { popoverProps, underlayProps } = usePopover(
{
containerPadding: OVERLAY_PADDING_FROM_CONTAINER,
maxHeight:
ITEM_HEIGHT * maxItemsUntilScroll + Y_PADDING_INSIDE_OVERLAY * 2 + 2,
maxHeight: getMenuPopoverMaxHeight({ maxItemsUntilScroll }),
offset: OVERLAY_OFFSET,
placement,
popoverRef,
Expand Down
6 changes: 5 additions & 1 deletion easy-ui-react/src/Menu/_mixins.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@use "../styles/common" as *;

@mixin root {
@mixin tokens {
@include component-token(
"menu",
"border_radius",
Expand Down Expand Up @@ -33,6 +33,10 @@
"color.border",
theme-token("color.neutral.300")
);
}

@mixin root {
@include tokens;

background: component-token("menu", "color.background");
border: design-token("shape.border_width.1") solid
Expand Down
8 changes: 8 additions & 0 deletions easy-ui-react/src/Menu/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ export const OVERLAY_OFFSET = 8;
export const OVERLAY_PADDING_FROM_CONTAINER = 12;
export const SELECT_ALL_KEY = "all";

export function getMenuPopoverMaxHeight({
maxItemsUntilScroll,
}: {
maxItemsUntilScroll: number;
}) {
return ITEM_HEIGHT * maxItemsUntilScroll + Y_PADDING_INSIDE_OVERLAY * 2 + 2;
}

export function getUnmergedPopoverStyles(
width: MenuOverlayWidth,
triggerWidth: number | null,
Expand Down
59 changes: 59 additions & 0 deletions easy-ui-react/src/MultiSelect/MultiSelect.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Canvas, Meta, ArgTypes, Controls } from "@storybook/blocks";
import { MultiSelect } from "./MultiSelect";
import * as MultiSelectStories from "./MultiSelect.stories";

<Meta of={MultiSelectStories} />

# MultiSelect

The `<MultiSelect />` component is an input with a dropdown that allows users to select multiple options from a list. It's customizable with various props such as `placeholder`, `maxItemsUntilScroll`, and `onSelectionChange`.

<Canvas of={MultiSelectStories.StandardDropdown} />

## Async Dropdown

The `<MultiSelect />` component can also support asynchronous loading of dropdown items. This is useful for fetching items from a server or a large dataset.

<Canvas of={MultiSelectStories.AsyncDropdown} />

## With Icons

This variant of the `<MultiSelect />` includes icons next to each option. It uses the `renderPill` prop to customize how selected items are displayed, and it can render custom content in the dropdown items.

<Canvas of={MultiSelectStories.WithIcons} />

## Disabled Keys

You can disable specific items from being selected by using the `disabledKeys` prop. This is useful when some options should be unselectable.

<Canvas of={MultiSelectStories.DisabledKeys} />

## Max Items Until Scroll

When there are too many items to fit in the dropdown, you can set a maximum number of items to display before the dropdown becomes scrollable. The `maxItemsUntilScroll` prop controls this behavior.

<Canvas of={MultiSelectStories.MaxItemsUntilScroll} />

## Properties

### MultiSelect

<ArgTypes of={MultiSelect} />

### MultiSelect.Pill

The `MultiSelect.Pill` component represents a selected item in the multi-select dropdown. It's used to display each selected item as a "pill" or tag.

<ArgTypes of={MultiSelect.Pill} />

### MultiSelect.Option

The `MultiSelect.Option` component represents a single option in the dropdown.

<ArgTypes of={MultiSelect.Option} />

### MultiSelect.OptionText

The `MultiSelect.OptionText` component represents the default text inside a dropdown option.

<ArgTypes of={MultiSelect.OptionText} />
78 changes: 78 additions & 0 deletions easy-ui-react/src/MultiSelect/MultiSelect.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
@use "../styles/common" as *;
@use "../InputField/mixins" as InputField;
@use "../Menu/mixins" as Menu;
@use "../styles/unstyled";

.MultiSelect {
@include InputField.root;
@include component-token("multi-select", "input-min-width", 120px);

position: relative;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: design-token("space.2");

padding: calc(#{design-token("space.1")} - 1px) design-token("space.2");
background-color: component-token("inputfield", "color.background");
border: design-token("shape.border_width.1") solid
component-token("inputfield", "color.border.resting");
border-radius: component-token("inputfield", "border_radius");
min-height: design-token("space.6");
width: 100%;

&:has(.input[data-focused="true"]) {
box-shadow: component-token("inputfield", "box_shadow");
border-color: component-token("inputfield", "color.border.engaged");
}
}

.comboBoxContainer {
display: flex;
flex: 1 1 0%;
}

.comboBox {
display: flex;
flex: 1 1 0%;
}

.inputContainer {
display: inline-flex;
padding-left: 0;
padding-right: 0;
flex-wrap: wrap;
flex: 1 1 0%;
align-items: center;
}

.input {
@include font-style("body1");
flex: 1 1 0%;
width: 100%;
min-width: component-token("multi-select", "input-min-width");
padding: design-token("space.1") 0;
margin: calc(#{design-token("space.1")} * -1) 0;
outline: none;
background-color: transparent;
border: 0;

&:focus,
&:active {
color: component-token("inputfield", "color.text.engaged");
}

&::placeholder {
color: component-token("inputfield", "color.text.subdued");
}
}

.dropdownArrowButton {
@include unstyled.button;
display: inline-flex;
align-items: center;
justify-content: center;
width: design-token("space.3");
height: design-token("space.3");
}
Loading

0 comments on commit 74272b9

Please sign in to comment.