Skip to content

Commit

Permalink
Multi-select CB (#2988)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshwooding authored Mar 14, 2024
1 parent 7c63a32 commit 242ed86
Show file tree
Hide file tree
Showing 17 changed files with 1,098 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const {
EmptyMessage,
ComplexOption,
ObjectValue,
MultiplePills,
MultiplePillsTruncated,
} = composeStories(comboBoxNextStories);

describe("Given a ComboBox", () => {
Expand Down Expand Up @@ -299,6 +301,7 @@ describe("Given a ComboBox", () => {
"aria-selected",
"true"
);
cy.findByRole("button", { name: /^Alabama/ }).should("be.visible");
cy.findByRole("option", { name: "Alaska" }).realClick();
cy.get("@selectionChange").should(
"have.been.calledWith",
Expand All @@ -310,6 +313,8 @@ describe("Given a ComboBox", () => {
"aria-selected",
"true"
);
cy.findByRole("button", { name: /^Alabama/ }).should("be.visible");
cy.findByRole("button", { name: /^Alaska/ }).should("be.visible");
cy.findByRole("listbox").should("exist");
cy.findByRole("combobox").should("have.value", "");
});
Expand All @@ -330,6 +335,7 @@ describe("Given a ComboBox", () => {
"aria-selected",
"true"
);
cy.findByRole("button", { name: /^Alabama/ }).should("be.visible");
cy.realPress("ArrowDown");
cy.realPress("Enter");
cy.get("@selectionChange").should(
Expand All @@ -342,6 +348,8 @@ describe("Given a ComboBox", () => {
"aria-selected",
"true"
);
cy.findByRole("button", { name: /^Alabama/ }).should("be.visible");
cy.findByRole("button", { name: /^Alaska/ }).should("be.visible");
cy.findByRole("listbox").should("exist");
cy.findByRole("combobox").should("have.value", "");
});
Expand Down Expand Up @@ -474,6 +482,46 @@ describe("Given a ComboBox", () => {
cy.findAllByRole("option").should("not.be.activeDescendant");
});

it("should wrap pills by default", () => {
cy.mount(<MultiplePills />);
cy.findAllByRole("button").should("have.length", 4).should("be.visible");
});

it("should truncate pills when `truncate=true` and expand them on focus", () => {
cy.mount(<MultiplePillsTruncated />);
cy.findAllByRole("button").should("have.length", 2).should("be.visible");
cy.findByTestId(/OverflowMenuIcon/i).should("be.visible");
cy.findByRole("combobox").realClick();
cy.findAllByRole("button").should("have.length", 4).should("be.visible");
});

it("should focus the pills first and on tab focus the input", () => {
cy.mount(<MultiplePills />);
cy.realPress("Tab");
cy.findByRole("button", { name: /^Alabama/ }).should("be.focused");
cy.realPress("Tab");
cy.findByRole("combobox").should("be.focused");
});

it("should support arrow key navigation between pills and input", () => {
cy.mount(<MultiplePills />);
cy.realPress("Tab");
cy.findByRole("button", { name: /^Alabama/ }).should("be.focused");
cy.realPress("ArrowRight");
cy.findByRole("button", { name: /^Alaska/ }).should("be.focused");
cy.realPress("ArrowRight");
cy.findByRole("button", { name: /^Arizona/ }).should("be.focused");
cy.realPress("ArrowRight");
cy.findByRole("combobox").should("be.focused");

cy.realPress("ArrowLeft");
cy.findByRole("button", { name: /^Arizona/ }).should("be.focused");
cy.realPress("ArrowLeft");
cy.findByRole("button", { name: /^Alaska/ }).should("be.focused");
cy.realPress("ArrowLeft");
cy.findByRole("button", { name: /^Alabama/ }).should("be.focused");
});

it("should render the custom floating component", () => {
cy.mount(
<CustomFloatingComponentProvider>
Expand Down
3 changes: 3 additions & 0 deletions packages/lab/src/combo-box-next/ComboBoxNext.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.saltComboBoxNext-focused {
outline: var(--salt-focused-outline);
}
43 changes: 38 additions & 5 deletions packages/lab/src/combo-box-next/ComboBoxNext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ import {
MouseEvent,
ReactNode,
Ref,
SyntheticEvent,
useEffect,
useRef,
} from "react";
import {
Button,
Input,
InputProps,
makePrefixer,
useFloatingComponent,
useFloatingUI,
Expand All @@ -36,14 +35,18 @@ import {
import { ChevronDownIcon, ChevronUpIcon } from "@salt-ds/icons";
import { useComboBoxNext, UseComboBoxNextProps } from "./useComboBoxNext";
import { OptionList } from "../option/OptionList";
import { PillInput, PillInputProps } from "../pill-input";
import { useWindow } from "@salt-ds/window";
import { useComponentCssInjection } from "@salt-ds/styles";
import comboBoxNextCss from "./ComboBoxNext.css";

export type ComboBoxNextProps<Item = string> = {
/**
* The options to display in the combo box.
*/
children?: ReactNode;
} & UseComboBoxNextProps<Item> &
InputProps;
PillInputProps;

const withBaseName = makePrefixer("saltComboBoxNext");

Expand Down Expand Up @@ -74,9 +77,17 @@ export const ComboBoxNext = forwardRef(function ComboBox<Item>(
value,
defaultValue,
valueToString = defaultValueToString,
truncate,
...rest
} = props;

const targetWindow = useWindow();
useComponentCssInjection({
testId: "salt-combo-box-next",
css: comboBoxNextCss,
window: targetWindow,
});

const {
a11yProps: { "aria-labelledby": formFieldLabelledBy } = {},
disabled: formFieldDisabled,
Expand Down Expand Up @@ -287,6 +298,12 @@ export const ComboBoxNext = forwardRef(function ComboBox<Item>(
onChange?.(event);
};

const handlePillRemove = (event: SyntheticEvent, index: number) => {
event.stopPropagation();
const removed = selectedState[index];
select(event, getOptionsMatching((option) => option.value === removed)[0]);
};

const handleListMouseOver = () => {
setFocusVisibleState(false);
};
Expand Down Expand Up @@ -346,8 +363,15 @@ export const ComboBoxNext = forwardRef(function ComboBox<Item>(

return (
<ListControlContext.Provider value={listControl}>
<Input
className={clsx(withBaseName(), className)}
<PillInput
className={clsx(
withBaseName(),
{
[withBaseName("focused")]: focusedState,
[withBaseName("focusVisible")]: focusVisibleState,
},
className
)}
endAdornment={
<>
{endAdornment}
Expand Down Expand Up @@ -395,6 +419,15 @@ export const ComboBoxNext = forwardRef(function ComboBox<Item>(
onFocus: handleFocus,
...rest,
})}
pills={
multiselect ? selectedState.map((item) => valueToString(item)) : []
}
truncate={truncate && !focusedState && !openState}
onPillRemove={handlePillRemove}
hidePillClose={!focusedState || readOnly}
emptyReadOnlyMarker={
readOnly && selectedState.length > 0 ? "" : undefined
}
/>
<FloatingComponent
open={(openState || focusedState) && !readOnly && children != undefined}
Expand Down
6 changes: 3 additions & 3 deletions packages/lab/src/dropdown-next/DropdownNext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
import { clsx } from "clsx";
import { useWindow } from "@salt-ds/window";
import { useComponentCssInjection } from "@salt-ds/styles";
import dropdownCss from "./DropdownNext.css";
import dropdownNextCss from "./DropdownNext.css";
import { ListControlContext } from "../list-control/ListControlContext";
import { OptionList } from "../option/OptionList";

Expand Down Expand Up @@ -130,8 +130,8 @@ export const DropdownNext = forwardRef(function DropdownNext<Item>(

const targetWindow = useWindow();
useComponentCssInjection({
testId: "salt-DropdownNext",
css: dropdownCss,
testId: "salt-dropdown-next",
css: dropdownNextCss,
window: targetWindow,
});

Expand Down
29 changes: 24 additions & 5 deletions packages/lab/src/option/Option.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@
gap: var(--salt-spacing-100);
position: relative;
align-items: center;
margin-top: var(--salt-size-border);
margin-bottom: var(--salt-size-border);
border-top: var(--salt-size-border) var(--salt-container-borderStyle) transparent;
border-bottom: var(--salt-size-border) var(--salt-container-borderStyle) transparent;
cursor: var(--salt-selectable-cursor-hover);
box-sizing: border-box;
}
Expand All @@ -33,7 +29,30 @@

.saltOption[aria-selected="true"] {
background: var(--salt-selectable-background-selected);
border-color: var(--salt-selectable-borderColor-selected);
}

.saltOption[aria-selected="true"]::before {
content: "";
display: block;
position: absolute;
top: -1px;
bottom: -1px;
left: 0;
width: 100%;
}

.saltOption[aria-selected="true"]::before {
border-top: var(--salt-size-border) var(--salt-selectable-borderStyle-selected) var(--salt-selectable-borderColor-selected);
border-bottom: var(--salt-size-border) var(--salt-selectable-borderStyle-selected) var(--salt-selectable-borderColor-selected);
}

.saltOption[aria-selected="true"]:first-of-type::before {
border-top: unset;
top: 0;
}
.saltOption[aria-selected="true"]:last-of-type::before {
border-bottom: unset;
bottom: 0;
}

.saltOption[aria-disabled="true"] {
Expand Down
3 changes: 3 additions & 0 deletions packages/lab/src/option/OptionList.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
max-height: inherit;
min-height: inherit;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: var(--salt-size-border);
}

.saltOptionList-collapsed {
Expand Down
Loading

0 comments on commit 242ed86

Please sign in to comment.