✨(react) add monoline props to multi select
We want to enable a mode that prevent the pills the wrap on multiple lines in order to control any height overflowing. In monoline mode the selected items are displayed as text to allow text ellipsis, and the menu renders the list items with checkboxes.
This commit is contained in:
@@ -157,6 +157,23 @@
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.c__select__menu--checkbox {
|
||||
.c__select__menu__item {
|
||||
padding: 0;
|
||||
|
||||
.c__checkbox {
|
||||
padding: 0.75rem;
|
||||
margin-left: 0;
|
||||
|
||||
&__label {
|
||||
font-size: var(--c--components--forms-select--item-font-size);
|
||||
color: var(--c--components--forms-select--item-color);
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Modifiers */
|
||||
@@ -233,13 +250,44 @@
|
||||
}
|
||||
|
||||
&--multi {
|
||||
.c__select__wrapper {
|
||||
height: auto;
|
||||
min-height: var(--c--components--forms-select--height);
|
||||
&.c__select--monoline {
|
||||
.c__select__inner {
|
||||
.c__select__inner__value {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c__select__inner__actions {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.c__select--multiline {
|
||||
.c__select__wrapper {
|
||||
height: auto;
|
||||
min-height: var(--c--components--forms-select--height);
|
||||
}
|
||||
|
||||
.c__select__inner {
|
||||
display: block;
|
||||
|
||||
&__actions {
|
||||
float: right;
|
||||
position: relative;
|
||||
height: 0;
|
||||
top: px-to-rem(3px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.c__select--text {
|
||||
.c__select__inner__value {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.c__select__inner {
|
||||
display: block;
|
||||
|
||||
&__value {
|
||||
gap: 0.25rem;
|
||||
@@ -298,13 +346,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
float: right;
|
||||
position: relative;
|
||||
height: 0;
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,21 +6,22 @@ import { FieldProps } from ":/components/Forms/Field";
|
||||
export * from ":/components/Forms/Select/mono";
|
||||
export * from ":/components/Forms/Select/multi";
|
||||
|
||||
export type OptionWithRender = {
|
||||
disabled?: boolean;
|
||||
type BaseOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
render: () => ReactNode;
|
||||
highlighted?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type Option =
|
||||
| {
|
||||
disabled?: boolean;
|
||||
value?: string;
|
||||
label: string;
|
||||
render?: undefined;
|
||||
}
|
||||
| OptionWithRender;
|
||||
export type OptionWithRender = BaseOption;
|
||||
|
||||
export type OptionWithoutRender = Omit<BaseOption, "value" | "render"> & {
|
||||
value?: string;
|
||||
render?: undefined;
|
||||
};
|
||||
|
||||
export type Option = OptionWithoutRender | OptionWithRender;
|
||||
|
||||
export interface SelectHandle {
|
||||
blur: () => void;
|
||||
@@ -45,6 +46,9 @@ export type SelectProps = PropsWithChildren &
|
||||
clearable?: boolean;
|
||||
multi?: boolean;
|
||||
showLabelWhenSelected?: boolean;
|
||||
monoline?: boolean;
|
||||
selectedItemsStyle?: "pills" | "text";
|
||||
menuOptionsStyle?: "plain" | "checkbox";
|
||||
};
|
||||
export const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
|
||||
if (props.defaultValue && props.value) {
|
||||
|
||||
@@ -29,6 +29,10 @@ export const optionToValue = (option: Option) => {
|
||||
return option.value ?? option.label;
|
||||
};
|
||||
|
||||
export const optionsEqual = (a: Option, b: Option) => {
|
||||
return optionToValue(a) === optionToValue(b);
|
||||
};
|
||||
|
||||
export const renderOption = (option: Option) => {
|
||||
if (isOptionWithRender(option)) {
|
||||
return option.render();
|
||||
|
||||
@@ -9,9 +9,9 @@ import { Option, SelectProps } from ":/components/Forms/Select";
|
||||
import {
|
||||
getOptionsFilter,
|
||||
optionToValue,
|
||||
renderOption,
|
||||
} from ":/components/Forms/Select/mono-common";
|
||||
import { SelectedOption } from ":/components/Forms/Select/utils";
|
||||
import { SelectedItems } from ":/components/Forms/Select/multi-selected-items";
|
||||
import { SelectMultiMenu } from ":/components/Forms/Select/multi-menu";
|
||||
|
||||
/**
|
||||
* This method returns a comparator that can be used to filter out options for multi select.
|
||||
@@ -43,19 +43,7 @@ export type SubProps = Omit<SelectProps, "onChange"> & {
|
||||
selectedItems: Option[];
|
||||
};
|
||||
|
||||
export const SelectMultiAux = ({
|
||||
options,
|
||||
labelAsPlaceholder,
|
||||
selectedItems,
|
||||
clearable = true,
|
||||
disabled,
|
||||
hideLabel,
|
||||
name,
|
||||
downshiftReturn,
|
||||
useMultipleSelectionReturn,
|
||||
children,
|
||||
...props
|
||||
}: SubProps & {
|
||||
export interface SelectMultiAuxProps extends SubProps {
|
||||
options: Option[];
|
||||
labelAsPlaceholder: boolean;
|
||||
selectedItems: Option[];
|
||||
@@ -70,9 +58,11 @@ export const SelectMultiAux = ({
|
||||
wrapperProps?: HTMLAttributes<HTMLDivElement>;
|
||||
};
|
||||
useMultipleSelectionReturn: ReturnType<typeof useMultipleSelection<Option>>;
|
||||
}) => {
|
||||
}
|
||||
|
||||
export const SelectMultiAux = ({ children, ...props }: SelectMultiAuxProps) => {
|
||||
const { t } = useCunningham();
|
||||
const labelProps = downshiftReturn.getLabelProps();
|
||||
const labelProps = props.downshiftReturn.getLabelProps();
|
||||
return (
|
||||
<Field {...props}>
|
||||
<div
|
||||
@@ -80,54 +70,60 @@ export const SelectMultiAux = ({
|
||||
"c__select",
|
||||
"c__select--multi",
|
||||
"c__select--" + props.state,
|
||||
"c__select--" + props.selectedItemsStyle,
|
||||
{
|
||||
"c__select--disabled": disabled,
|
||||
"c__select--populated": selectedItems.length > 0,
|
||||
"c__select--disabled": props.disabled,
|
||||
"c__select--populated": props.selectedItems.length > 0,
|
||||
"c__select--monoline": props.monoline,
|
||||
"c__select--multiline": !props.monoline,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames("c__select__wrapper", {
|
||||
"c__select__wrapper--focus": downshiftReturn.isOpen && !disabled,
|
||||
"c__select__wrapper--focus":
|
||||
props.downshiftReturn.isOpen && !props.disabled,
|
||||
})}
|
||||
{...downshiftReturn.wrapperProps}
|
||||
{...props.downshiftReturn.wrapperProps}
|
||||
>
|
||||
{selectedItems.map((selectedItem, index) => (
|
||||
{props.selectedItems.map((selectedItem, index) => (
|
||||
<input
|
||||
key={`${optionToValue(selectedItem)}${index.toString()}`}
|
||||
type="hidden"
|
||||
name={name}
|
||||
name={props.name}
|
||||
value={optionToValue(selectedItem)}
|
||||
/>
|
||||
))}
|
||||
<LabelledBox
|
||||
label={props.label}
|
||||
labelAsPlaceholder={labelAsPlaceholder}
|
||||
labelAsPlaceholder={props.labelAsPlaceholder}
|
||||
htmlFor={labelProps.htmlFor}
|
||||
labelId={labelProps.id}
|
||||
hideLabel={hideLabel}
|
||||
disabled={disabled}
|
||||
hideLabel={props.hideLabel}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
<div className="c__select__inner">
|
||||
<div className="c__select__inner__actions">
|
||||
{clearable && !disabled && selectedItems.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
color="tertiary"
|
||||
size="nano"
|
||||
aria-label={t(
|
||||
"components.forms.select.clear_all_button_aria_label",
|
||||
)}
|
||||
className="c__select__inner__actions__clear"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onSelectedItemsChange([]);
|
||||
}}
|
||||
icon={<span className="material-icons">close</span>}
|
||||
/>
|
||||
<div className="c__select__inner__actions__separator" />
|
||||
</>
|
||||
)}
|
||||
{props.clearable &&
|
||||
!props.disabled &&
|
||||
props.selectedItems.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
color="tertiary"
|
||||
size="nano"
|
||||
aria-label={t(
|
||||
"components.forms.select.clear_all_button_aria_label",
|
||||
)}
|
||||
className="c__select__inner__actions__clear"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onSelectedItemsChange([]);
|
||||
}}
|
||||
icon={<span className="material-icons">close</span>}
|
||||
/>
|
||||
<div className="c__select__inner__actions__separator" />
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
color="tertiary"
|
||||
size="nano"
|
||||
@@ -135,95 +131,24 @@ export const SelectMultiAux = ({
|
||||
icon={
|
||||
<span
|
||||
className={classNames("material-icons", {
|
||||
opened: downshiftReturn.isOpen,
|
||||
opened: props.downshiftReturn.isOpen,
|
||||
})}
|
||||
>
|
||||
arrow_drop_down
|
||||
</span>
|
||||
}
|
||||
disabled={disabled}
|
||||
{...downshiftReturn.toggleButtonProps}
|
||||
disabled={props.disabled}
|
||||
{...props.downshiftReturn.toggleButtonProps}
|
||||
/>
|
||||
</div>
|
||||
<div className="c__select__inner__value">
|
||||
{selectedItems.map((selectedItemForRender, index) => {
|
||||
return (
|
||||
<div
|
||||
className="c__select__inner__value__pill"
|
||||
key={`${optionToValue(
|
||||
selectedItemForRender,
|
||||
)}${index.toString()}`}
|
||||
{...useMultipleSelectionReturn.getSelectedItemProps({
|
||||
selectedItem: selectedItemForRender,
|
||||
index,
|
||||
})}
|
||||
>
|
||||
<SelectedOption
|
||||
option={selectedItemForRender}
|
||||
{...props}
|
||||
/>
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
color="tertiary"
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
aria-label={t(
|
||||
"components.forms.select.clear_button_aria_label",
|
||||
)}
|
||||
type="button"
|
||||
className="c__select__inner__value__pill__clear"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
useMultipleSelectionReturn.removeSelectedItem(
|
||||
selectedItemForRender,
|
||||
);
|
||||
}}
|
||||
icon={<span className="material-icons">close</span>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<SelectedItems {...props} />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</LabelledBox>
|
||||
</div>
|
||||
<div
|
||||
className={classNames("c__select__menu", {
|
||||
"c__select__menu--opened": downshiftReturn.isOpen,
|
||||
})}
|
||||
{...downshiftReturn.getMenuProps()}
|
||||
>
|
||||
<ul>
|
||||
{downshiftReturn.isOpen && (
|
||||
<>
|
||||
{options.map((option, index) => {
|
||||
const isActive = index === downshiftReturn.highlightedIndex;
|
||||
return (
|
||||
<li
|
||||
className={classNames("c__select__menu__item", {
|
||||
"c__select__menu__item--highlight": isActive,
|
||||
"c__select__menu__item--disabled": option.disabled,
|
||||
})}
|
||||
key={`${optionToValue(option)}${index.toString()}`}
|
||||
{...downshiftReturn.getItemProps({
|
||||
item: option,
|
||||
index,
|
||||
})}
|
||||
>
|
||||
<span>{renderOption(option)}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{options.length === 0 && (
|
||||
<li className="c__select__menu__item c__select__menu__empty-placeholder">
|
||||
{t("components.forms.select.menu_empty_placeholder")}
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<SelectMultiMenu {...props} />
|
||||
</div>
|
||||
</Field>
|
||||
);
|
||||
|
||||
100
packages/react/src/components/Forms/Select/multi-menu.tsx
Normal file
100
packages/react/src/components/Forms/Select/multi-menu.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { SelectMultiAuxProps } from ":/components/Forms/Select/multi-common";
|
||||
import {
|
||||
optionToValue,
|
||||
renderOption,
|
||||
} from ":/components/Forms/Select/mono-common";
|
||||
import { useCunningham } from ":/components/Provider";
|
||||
import { Checkbox } from ":/components/Forms/Checkbox";
|
||||
import { Option } from ":/components/Forms/Select/index";
|
||||
|
||||
export const SelectMultiMenu = (props: SelectMultiAuxProps) => {
|
||||
const { t } = useCunningham();
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"c__select__menu",
|
||||
"c__select__menu--" + props.menuOptionsStyle,
|
||||
{
|
||||
"c__select__menu--opened": props.downshiftReturn.isOpen,
|
||||
},
|
||||
)}
|
||||
{...props.downshiftReturn.getMenuProps()}
|
||||
>
|
||||
<ul>
|
||||
{props.downshiftReturn.isOpen && (
|
||||
<>
|
||||
{props.options.map((option, index) => (
|
||||
<MenuItem
|
||||
{...props}
|
||||
option={option}
|
||||
index={index}
|
||||
key={optionToValue(option)}
|
||||
/>
|
||||
))}
|
||||
{props.options.length === 0 && (
|
||||
<li className="c__select__menu__item c__select__menu__empty-placeholder">
|
||||
{t("components.forms.select.menu_empty_placeholder")}
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type MenuItemProps = SelectMultiAuxProps & { option: Option; index: number };
|
||||
|
||||
const MenuItem = (props: MenuItemProps) => {
|
||||
if (props.menuOptionsStyle === "plain") {
|
||||
return <MenuItemPlain {...props} />;
|
||||
}
|
||||
if (props.menuOptionsStyle === "checkbox") {
|
||||
return <MenuItemCheckbox {...props} />;
|
||||
}
|
||||
throw new Error("Unknown menuOptionsStyle");
|
||||
};
|
||||
|
||||
const MenuItemPlain = ({ option, index, ...props }: MenuItemProps) => {
|
||||
const isHighlighted =
|
||||
index === props.downshiftReturn.highlightedIndex || option.highlighted;
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classNames("c__select__menu__item", {
|
||||
"c__select__menu__item--highlight": isHighlighted,
|
||||
"c__select__menu__item--disabled": option.disabled,
|
||||
})}
|
||||
{...props.downshiftReturn.getItemProps({
|
||||
item: option,
|
||||
index,
|
||||
})}
|
||||
>
|
||||
<span>{renderOption(option)}</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuItemCheckbox = ({ option, index, ...props }: MenuItemProps) => {
|
||||
return (
|
||||
<li
|
||||
className={classNames("c__select__menu__item", {
|
||||
"c__select__menu__item--highlight":
|
||||
index === props.downshiftReturn.highlightedIndex,
|
||||
"c__select__menu__item--disabled": option.disabled,
|
||||
})}
|
||||
{...props.downshiftReturn.getItemProps({
|
||||
item: option,
|
||||
index,
|
||||
})}
|
||||
>
|
||||
<Checkbox
|
||||
label={renderOption(option) as any}
|
||||
checked={option.highlighted}
|
||||
fullWidth={true}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -127,6 +127,7 @@ export const SelectMultiSearchable = forwardRef<SelectHandle, SubProps>(
|
||||
return (
|
||||
<SelectMultiAux
|
||||
{...props}
|
||||
monoline={false}
|
||||
options={options}
|
||||
labelAsPlaceholder={labelAsPlaceholder}
|
||||
selectedItems={props.selectedItems}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
import { useCunningham } from ":/components/Provider";
|
||||
import {
|
||||
optionToString,
|
||||
optionToValue,
|
||||
} from ":/components/Forms/Select/mono-common";
|
||||
import { SelectedOption } from ":/components/Forms/Select/utils";
|
||||
import { Button } from ":/components/Button";
|
||||
import { SelectMultiAuxProps } from ":/components/Forms/Select/multi-common";
|
||||
|
||||
export const SelectedItems = (props: SelectMultiAuxProps) => {
|
||||
if (props.selectedItemsStyle === "pills") {
|
||||
return <SelectedItemsChips {...props} />;
|
||||
}
|
||||
if (props.selectedItemsStyle === "text") {
|
||||
return <SelectedItemsText {...props} />;
|
||||
}
|
||||
throw new Error("Unknown selectedItemsStyle");
|
||||
};
|
||||
|
||||
const SelectedItemsChips = ({
|
||||
selectedItems,
|
||||
selectedItemsStyle,
|
||||
disabled,
|
||||
useMultipleSelectionReturn,
|
||||
...props
|
||||
}: SelectMultiAuxProps) => {
|
||||
const { t } = useCunningham();
|
||||
return selectedItems.map((selectedItemForRender, index) => {
|
||||
return (
|
||||
<div
|
||||
className="c__select__inner__value__pill"
|
||||
key={`${optionToValue(selectedItemForRender)}${index.toString()}`}
|
||||
{...useMultipleSelectionReturn.getSelectedItemProps({
|
||||
selectedItem: selectedItemForRender,
|
||||
index,
|
||||
})}
|
||||
>
|
||||
<SelectedOption option={selectedItemForRender} {...props} />
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
color="tertiary"
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
aria-label={t("components.forms.select.clear_button_aria_label")}
|
||||
type="button"
|
||||
className="c__select__inner__value__pill__clear"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
useMultipleSelectionReturn.removeSelectedItem(
|
||||
selectedItemForRender,
|
||||
);
|
||||
}}
|
||||
icon={<span className="material-icons">close</span>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const SelectedItemsText = ({ selectedItems }: SelectMultiAuxProps) => {
|
||||
return selectedItems.map((item) => optionToString(item)).join(", ");
|
||||
};
|
||||
@@ -5,16 +5,30 @@ import {
|
||||
SelectMultiAux,
|
||||
SubProps,
|
||||
} from ":/components/Forms/Select/multi-common";
|
||||
import { optionToString } from ":/components/Forms/Select/mono-common";
|
||||
import { SelectHandle } from ":/components/Forms/Select/index";
|
||||
import {
|
||||
optionsEqual,
|
||||
optionToString,
|
||||
} from ":/components/Forms/Select/mono-common";
|
||||
import { Option, SelectHandle } from ":/components/Forms/Select/index";
|
||||
|
||||
export const SelectMultiSimple = forwardRef<SelectHandle, SubProps>(
|
||||
(props, ref) => {
|
||||
const options = React.useMemo(
|
||||
() =>
|
||||
props.options.filter(getMultiOptionsFilter(props.selectedItems, "")),
|
||||
[props.selectedItems],
|
||||
);
|
||||
const isSelected = (option: Option) =>
|
||||
!!props.selectedItems.find((selectedItem) =>
|
||||
optionsEqual(selectedItem, option),
|
||||
);
|
||||
|
||||
const options = React.useMemo(() => {
|
||||
if (props.monoline) {
|
||||
return props.options.map((option) => ({
|
||||
...option,
|
||||
highlighted: isSelected(option),
|
||||
}));
|
||||
}
|
||||
return props.options.filter(
|
||||
getMultiOptionsFilter(props.selectedItems, ""),
|
||||
);
|
||||
}, [props.selectedItems]);
|
||||
|
||||
const useMultipleSelectionReturn = useMultipleSelection({
|
||||
selectedItems: props.selectedItems,
|
||||
@@ -47,7 +61,7 @@ export const SelectMultiSimple = forwardRef<SelectHandle, SubProps>(
|
||||
return {
|
||||
...changes,
|
||||
isOpen: true, // keep the menu open after selection.
|
||||
highlightedIndex: 0, // with the first option highlighted.
|
||||
highlightedIndex: state.highlightedIndex, // avoid automatic scroll up on click.
|
||||
};
|
||||
}
|
||||
return changes;
|
||||
@@ -57,7 +71,18 @@ export const SelectMultiSimple = forwardRef<SelectHandle, SubProps>(
|
||||
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
|
||||
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
|
||||
case useSelect.stateChangeTypes.ItemClick:
|
||||
if (newSelectedItem) {
|
||||
if (!newSelectedItem) {
|
||||
break;
|
||||
}
|
||||
if (isSelected(newSelectedItem)) {
|
||||
// Remove the item if it is already selected.
|
||||
props.onSelectedItemsChange(
|
||||
props.selectedItems.filter(
|
||||
(selectedItem) =>
|
||||
!optionsEqual(selectedItem, newSelectedItem),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
props.onSelectedItemsChange([
|
||||
...props.selectedItems,
|
||||
newSelectedItem,
|
||||
@@ -86,6 +111,8 @@ export const SelectMultiSimple = forwardRef<SelectHandle, SubProps>(
|
||||
options={options}
|
||||
labelAsPlaceholder={props.selectedItems.length === 0}
|
||||
selectedItems={props.selectedItems}
|
||||
selectedItemsStyle={props.monoline ? "text" : "pills"}
|
||||
menuOptionsStyle={props.monoline ? "checkbox" : "plain"}
|
||||
downshiftReturn={{
|
||||
...downshiftReturn,
|
||||
wrapperProps: {
|
||||
|
||||
@@ -88,6 +88,18 @@ For some reasons you might want to hide the label of the Multi-Select. You can d
|
||||
<Story id="components-forms-select-multi--hidden-label"/>
|
||||
</Canvas>
|
||||
|
||||
## Monoline
|
||||
|
||||
You can use `monoline` props in order to make sure the component will never wrap on multiple lines.
|
||||
When using this mode the selected options will be kept in the menu with checkboxes in order to make sure the user can
|
||||
always see the full list of selected options without having to scroll.
|
||||
|
||||
> At the moment this props cannot be used in conjunction with `searchable` props.
|
||||
|
||||
<Canvas sourceState="shown">
|
||||
<Story id="components-forms-select-multi--monoline"/>
|
||||
</Canvas>
|
||||
|
||||
## Custom render option
|
||||
|
||||
You can give customize the look of the options by providing `render` callback.
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
expectOptionToBeDisabled,
|
||||
expectOptionToBeUnselected,
|
||||
expectSelectedOptions,
|
||||
expectSelectedOptionsText,
|
||||
} from ":/components/Forms/Select/test-utils";
|
||||
import { Button } from ":/components/Button";
|
||||
|
||||
@@ -938,6 +939,139 @@ describe("<Select multi={true} />", () => {
|
||||
name: "London flag",
|
||||
});
|
||||
});
|
||||
|
||||
it("renders checkboxes in the menu and selected options as text when using monoline mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CunninghamProvider>
|
||||
<Select
|
||||
label="Cities"
|
||||
options={[
|
||||
{
|
||||
label: "Paris",
|
||||
},
|
||||
{
|
||||
label: "Panama",
|
||||
},
|
||||
{
|
||||
label: "London",
|
||||
},
|
||||
{
|
||||
label: "New York",
|
||||
},
|
||||
{
|
||||
label: "Tokyo",
|
||||
},
|
||||
]}
|
||||
multi={true}
|
||||
monoline={true}
|
||||
/>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
const input = screen.getByRole("combobox", {
|
||||
name: "Cities",
|
||||
});
|
||||
const menu: HTMLDivElement = screen.getByRole("listbox", {
|
||||
name: "Cities",
|
||||
});
|
||||
const label = screen.getByText("Cities")!.parentElement!;
|
||||
|
||||
const expectCheckedOptions = ({
|
||||
checked,
|
||||
notChecked,
|
||||
}: {
|
||||
checked: string[];
|
||||
notChecked: string[];
|
||||
}) => {
|
||||
checked.forEach((option) => {
|
||||
const optionElement = screen.getByRole("option", { name: option });
|
||||
const checkbox: HTMLInputElement =
|
||||
within(optionElement).getByRole("checkbox");
|
||||
expect(checkbox.checked).toEqual(true);
|
||||
});
|
||||
notChecked.forEach((option) => {
|
||||
const optionElement = screen.getByRole("option", { name: option });
|
||||
const checkbox: HTMLInputElement =
|
||||
within(optionElement).getByRole("checkbox");
|
||||
expect(checkbox.checked).toEqual(false);
|
||||
});
|
||||
};
|
||||
|
||||
// Expect no options to be selected.
|
||||
expectSelectedOptionsText([]);
|
||||
|
||||
// Make sure the label is set as placeholder.
|
||||
expect(Array.from(label.classList)).toContain("placeholder");
|
||||
|
||||
await user.click(input);
|
||||
|
||||
// Make sure the menu is opened and options are rendered.
|
||||
expectMenuToBeOpen(menu);
|
||||
expectOptions(["Paris", "Panama", "London", "New York", "Tokyo"]);
|
||||
|
||||
expectCheckedOptions({
|
||||
checked: [],
|
||||
notChecked: ["Paris", "Panama", "London", "New York", "Tokyo"],
|
||||
});
|
||||
|
||||
// Make sure the option is not selected.
|
||||
const option: HTMLLIElement = screen.getByRole("option", {
|
||||
name: "London",
|
||||
});
|
||||
expectOptionToBeUnselected(option);
|
||||
|
||||
// Select an option.
|
||||
await user.click(option);
|
||||
|
||||
// Make sure the option is selected.
|
||||
expectSelectedOptionsText(["London"]);
|
||||
|
||||
// Make sure the menu stays open.
|
||||
expectMenuToBeOpen(menu);
|
||||
|
||||
// Make sure the option is still present in the menu and is highlighted.
|
||||
expectCheckedOptions({
|
||||
checked: ["London"],
|
||||
notChecked: ["Paris", "Panama", "New York", "Tokyo"],
|
||||
});
|
||||
|
||||
// Select Paris
|
||||
await user.click(screen.getByRole("option", { name: "Paris" }));
|
||||
|
||||
// Make sure the option is selected.
|
||||
expectSelectedOptionsText(["London", "Paris"]);
|
||||
|
||||
screen.logTestingPlaygroundURL();
|
||||
|
||||
// Make sure the menu stays open.
|
||||
expectMenuToBeOpen(menu);
|
||||
|
||||
// Make sure the option is still present in the menu and is highlighted.
|
||||
expectCheckedOptions({
|
||||
checked: ["London", "Paris"],
|
||||
notChecked: ["Panama", "New York", "Tokyo"],
|
||||
});
|
||||
|
||||
// Click on London and make sure it is removed from the selected.
|
||||
await user.click(screen.getByRole("option", { name: "London" }));
|
||||
|
||||
// We need to move the cursor away from London otherwise the option will be highlighted.
|
||||
await user.hover(screen.getByRole("option", { name: "Paris" }));
|
||||
|
||||
// Make sure the option is not selected anymore.
|
||||
expectSelectedOptionsText(["Paris"]);
|
||||
|
||||
// Make sure the menu stays open.
|
||||
expectMenuToBeOpen(menu);
|
||||
|
||||
// Make sure the option is still present in the menu and is not highlighted anymore.
|
||||
await waitFor(() =>
|
||||
expectCheckedOptions({
|
||||
checked: ["Paris"],
|
||||
notChecked: ["London", "Panama", "New York", "Tokyo"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Searchable", async () => {
|
||||
|
||||
@@ -50,6 +50,17 @@ export const Disabled = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Monoline = {
|
||||
render: Template,
|
||||
args: {
|
||||
label: "Select cities",
|
||||
options: OPTIONS,
|
||||
defaultValue: [OPTIONS[4].value, OPTIONS[2].value, OPTIONS[1].value],
|
||||
monoline: true,
|
||||
clearable: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithText = {
|
||||
render: Template,
|
||||
args: {
|
||||
|
||||
@@ -44,11 +44,17 @@ export const SelectMulti = forwardRef<SelectHandle, SelectMultiProps>(
|
||||
newSelectedItems,
|
||||
) => {
|
||||
setSelectedItems(newSelectedItems);
|
||||
// props.onSelectedItemsChange?.(newSelectedItems);
|
||||
};
|
||||
|
||||
const defaultProps: Partial<SelectMultiProps> = {
|
||||
selectedItemsStyle: "pills",
|
||||
menuOptionsStyle: "plain",
|
||||
clearable: true,
|
||||
};
|
||||
|
||||
return props.searchable ? (
|
||||
<SelectMultiSearchable
|
||||
{...defaultProps}
|
||||
{...props}
|
||||
selectedItems={selectedItems}
|
||||
onSelectedItemsChange={onSelectedItemsChange}
|
||||
@@ -56,6 +62,7 @@ export const SelectMulti = forwardRef<SelectHandle, SelectMultiProps>(
|
||||
/>
|
||||
) : (
|
||||
<SelectMultiSimple
|
||||
{...defaultProps}
|
||||
{...props}
|
||||
selectedItems={selectedItems}
|
||||
onSelectedItemsChange={onSelectedItemsChange}
|
||||
|
||||
@@ -44,3 +44,8 @@ export const expectSelectedOptions = (expectedOptions: string[]) => {
|
||||
});
|
||||
expect(actualOptions).toEqual(expectedOptions);
|
||||
};
|
||||
|
||||
export const expectSelectedOptionsText = (expectedOptions: string[]) => {
|
||||
const valueElement = document.querySelector(".c__select__inner__value");
|
||||
expect(valueElement?.textContent).toEqual(expectedOptions.join(", "));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user